@mcoda/mswarm 0.1.101 → 0.1.103

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/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,17 @@ 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",
208
+ "fps",
209
+ "video_frames",
210
+ "high_noise_steps",
200
211
  "stream",
201
212
  "tools",
202
213
  "tool_choice",
@@ -204,6 +215,20 @@ const RESERVED_LOCAL_RUNNER_EXTRA_BODY_KEYS = new Set([
204
215
  "max_tokens",
205
216
  "temperature"
206
217
  ]);
218
+ const REQUIRED_GENERATIVE_OPERATION_REQUEST_PARAMETERS = {
219
+ "chat.completions": ["model", "messages"],
220
+ "images.generations": ["model", "prompt", "response_format"],
221
+ "audio.generations": ["model", "prompt", "response_format"],
222
+ "videos.generations": ["model", "prompt", "response_format"]
223
+ };
224
+ const AUDIO_GENERATION_RESPONSE_FORMATS = new Set(["wav", "flac", "mp3", "ogg"]);
225
+ const VIDEO_GENERATION_RESPONSE_FORMATS = new Set(["webm", "webp", "avi"]);
226
+ const STABLE_DIFFUSION_CPP_EXTRA_ARGS_TAG_PATTERN = /<sd_cpp_extra_args\b[^>]*>[\s\S]*?<\/sd_cpp_extra_args\s*>/gi;
227
+ const STABLE_DIFFUSION_CPP_EXTRA_ARGS_TAG_FRAGMENT_PATTERN = /<\/?sd_cpp_extra_args\b[^>]*>/gi;
228
+ const DEFAULT_IMAGE_GENERATIVE_OUTPUT_BYTES = 64 * 1024 * 1024;
229
+ const DEFAULT_AUDIO_GENERATIVE_OUTPUT_BYTES = 64 * 1024 * 1024;
230
+ const DEFAULT_VIDEO_GENERATIVE_OUTPUT_BYTES = 256 * 1024 * 1024;
231
+ const GENERATIVE_RESPONSE_JSON_OVERHEAD_BYTES = 64 * 1024;
207
232
  const LOCAL_RUNNER_KIND_ALIASES = {
208
233
  vllm: "vllm",
209
234
  "llama-cpp": "llama-cpp",
@@ -226,6 +251,13 @@ const LOCAL_RUNNER_KIND_ALIASES = {
226
251
  text_generation_inference: "tgi",
227
252
  custom: "custom"
228
253
  };
254
+ const GENERATIVE_RUNNER_KIND_ALIASES = {
255
+ "stable-diffusion-cpp": "stable-diffusion-cpp",
256
+ "stable-diffusion.cpp": "stable-diffusion-cpp",
257
+ stable_diffusion_cpp: "stable-diffusion-cpp",
258
+ "sd-cpp": "stable-diffusion-cpp",
259
+ "sd.cpp": "stable-diffusion-cpp"
260
+ };
229
261
  const LOCAL_RUNNER_AUTH_MODE_ALIASES = {
230
262
  none: "none",
231
263
  bearer: "bearer",
@@ -254,6 +286,189 @@ function optionalText(value) {
254
286
  function isRecord(value) {
255
287
  return typeof value === "object" && value !== null && !Array.isArray(value);
256
288
  }
289
+ export function normalizeSelfHostedGenerativeOperationName(value) {
290
+ const normalized = optionalText(value);
291
+ return normalized === "chat.completions" ||
292
+ normalized === "images.generations" ||
293
+ normalized === "audio.generations" ||
294
+ normalized === "videos.generations"
295
+ ? normalized
296
+ : null;
297
+ }
298
+ export function resolveSelfHostedInvocationOperation(value) {
299
+ const raw = optionalText(value);
300
+ if (!raw) {
301
+ return "chat.completions";
302
+ }
303
+ const operation = normalizeSelfHostedGenerativeOperationName(raw);
304
+ if (!operation) {
305
+ throw new Error(`unsupported self-hosted operation ${raw}`);
306
+ }
307
+ return operation;
308
+ }
309
+ function normalizeGenerativeOperationDescriptor(value) {
310
+ const record = isRecord(value)
311
+ ? {
312
+ ...value,
313
+ operation: value.operation ?? value.type,
314
+ requestParameterAllowlist: value.requestParameterAllowlist ??
315
+ value.request_parameter_allowlist ??
316
+ value.supported_parameters,
317
+ responseFormats: value.responseFormats ?? value.response_formats,
318
+ outputMimeTypes: value.outputMimeTypes ?? value.output_mime_types
319
+ }
320
+ : value;
321
+ const normalized = normalizeSelfHostedGenerativeOperation(record);
322
+ if (!normalized)
323
+ return null;
324
+ const operation = normalized.operation;
325
+ const requestParameterAllowlist = Array.from(new Set([
326
+ ...REQUIRED_GENERATIVE_OPERATION_REQUEST_PARAMETERS[operation],
327
+ ...(normalized.requestParameterAllowlist ??
328
+ GENERATIVE_REQUEST_PARAMETER_ALLOWLISTS[operation])
329
+ ]));
330
+ const responseFormats = normalized.responseFormats?.length
331
+ ? normalized.responseFormats
332
+ : operation === "chat.completions"
333
+ ? undefined
334
+ : operation === "images.generations"
335
+ ? ["b64_json"]
336
+ : operation === "audio.generations"
337
+ ? ["wav", "flac", "mp3", "ogg"]
338
+ : ["webm", "webp", "avi"];
339
+ return {
340
+ operation,
341
+ path: normalized.path,
342
+ method: "POST",
343
+ requestParameterAllowlist,
344
+ ...(responseFormats?.length ? { responseFormats } : {}),
345
+ ...(normalized.outputMimeTypes?.length ? { outputMimeTypes: normalized.outputMimeTypes } : {}),
346
+ ...(normalized.limits ? { limits: normalized.limits } : {})
347
+ };
348
+ }
349
+ function defaultGenerativeOperationDescriptor(operation, path = GENERATIVE_OPERATION_DEFAULT_PATHS[operation]) {
350
+ return {
351
+ operation,
352
+ path,
353
+ method: "POST",
354
+ requestParameterAllowlist: [...GENERATIVE_REQUEST_PARAMETER_ALLOWLISTS[operation]],
355
+ ...(operation === "images.generations"
356
+ ? { responseFormats: ["b64_json"] }
357
+ : operation === "audio.generations"
358
+ ? { responseFormats: ["wav", "flac", "mp3", "ogg"] }
359
+ : operation === "videos.generations"
360
+ ? { responseFormats: ["webm", "webp", "avi"] }
361
+ : {})
362
+ };
363
+ }
364
+ function normalizeGenerativeOperations(value) {
365
+ if (value === undefined)
366
+ return undefined;
367
+ if (!Array.isArray(value))
368
+ return [];
369
+ const operations = value
370
+ .map((entry) => normalizeGenerativeOperationDescriptor(entry))
371
+ .filter((entry) => Boolean(entry));
372
+ const deduplicated = new Map();
373
+ for (const operation of operations) {
374
+ if (!deduplicated.has(operation.operation)) {
375
+ deduplicated.set(operation.operation, operation);
376
+ }
377
+ }
378
+ return Array.from(deduplicated.values());
379
+ }
380
+ function inferGenerativeModalities(operations, direction) {
381
+ const modalities = new Set();
382
+ for (const descriptor of operations) {
383
+ if (direction === "input" || descriptor.operation === "chat.completions") {
384
+ modalities.add("text");
385
+ }
386
+ if (direction === "output" && descriptor.operation === "images.generations") {
387
+ modalities.add("image");
388
+ }
389
+ if (direction === "output" && descriptor.operation === "audio.generations") {
390
+ modalities.add("audio");
391
+ }
392
+ if (direction === "output" && descriptor.operation === "videos.generations") {
393
+ modalities.add("video");
394
+ }
395
+ }
396
+ return Array.from(modalities);
397
+ }
398
+ function generativeOperationCatalogInput(descriptor) {
399
+ const limits = descriptor.limits
400
+ ? {
401
+ ...(descriptor.limits.maxRequestBytes !== undefined
402
+ ? { max_request_bytes: descriptor.limits.maxRequestBytes }
403
+ : {}),
404
+ ...(descriptor.limits.maxOutputBytes !== undefined
405
+ ? { max_output_bytes: descriptor.limits.maxOutputBytes }
406
+ : {}),
407
+ ...(descriptor.limits.maxPromptChars !== undefined
408
+ ? { max_prompt_chars: descriptor.limits.maxPromptChars }
409
+ : {}),
410
+ ...(descriptor.limits.maxNegativePromptChars !== undefined
411
+ ? { max_negative_prompt_chars: descriptor.limits.maxNegativePromptChars }
412
+ : {}),
413
+ ...(descriptor.limits.maxN !== undefined ? { max_n: descriptor.limits.maxN } : {}),
414
+ ...(descriptor.limits.minWidth !== undefined
415
+ ? { min_width: descriptor.limits.minWidth }
416
+ : {}),
417
+ ...(descriptor.limits.maxWidth !== undefined
418
+ ? { max_width: descriptor.limits.maxWidth }
419
+ : {}),
420
+ ...(descriptor.limits.minHeight !== undefined
421
+ ? { min_height: descriptor.limits.minHeight }
422
+ : {}),
423
+ ...(descriptor.limits.maxHeight !== undefined
424
+ ? { max_height: descriptor.limits.maxHeight }
425
+ : {}),
426
+ ...(descriptor.limits.maxPixels !== undefined
427
+ ? { max_pixels: descriptor.limits.maxPixels }
428
+ : {}),
429
+ ...(descriptor.limits.minDurationSeconds !== undefined
430
+ ? { min_duration_seconds: descriptor.limits.minDurationSeconds }
431
+ : {}),
432
+ ...(descriptor.limits.maxDurationSeconds !== undefined
433
+ ? { max_duration_seconds: descriptor.limits.maxDurationSeconds }
434
+ : {}),
435
+ ...(descriptor.limits.maxSampleRate !== undefined
436
+ ? { max_sample_rate: descriptor.limits.maxSampleRate }
437
+ : {}),
438
+ ...(descriptor.limits.minFps !== undefined
439
+ ? { min_fps: descriptor.limits.minFps }
440
+ : {}),
441
+ ...(descriptor.limits.maxFps !== undefined
442
+ ? { max_fps: descriptor.limits.maxFps }
443
+ : {}),
444
+ ...(descriptor.limits.minVideoFrames !== undefined
445
+ ? { min_video_frames: descriptor.limits.minVideoFrames }
446
+ : {}),
447
+ ...(descriptor.limits.maxVideoFrames !== undefined
448
+ ? { max_video_frames: descriptor.limits.maxVideoFrames }
449
+ : {}),
450
+ ...(descriptor.limits.maxSteps !== undefined
451
+ ? { max_steps: descriptor.limits.maxSteps }
452
+ : {}),
453
+ ...(descriptor.limits.maxHighNoiseSteps !== undefined
454
+ ? { max_high_noise_steps: descriptor.limits.maxHighNoiseSteps }
455
+ : {})
456
+ }
457
+ : undefined;
458
+ return {
459
+ type: descriptor.operation,
460
+ path: descriptor.path,
461
+ method: descriptor.method,
462
+ supported_parameters: [...descriptor.requestParameterAllowlist],
463
+ ...(descriptor.responseFormats?.length
464
+ ? { response_formats: [...descriptor.responseFormats] }
465
+ : {}),
466
+ ...(descriptor.outputMimeTypes?.length
467
+ ? { output_mime_types: [...descriptor.outputMimeTypes] }
468
+ : {}),
469
+ ...(limits ? { limits } : {})
470
+ };
471
+ }
257
472
  function normalizeLookupKey(value) {
258
473
  return value.trim().toLowerCase();
259
474
  }
@@ -316,6 +531,10 @@ function normalizeLocalRunnerKind(value) {
316
531
  const normalized = optionalText(value);
317
532
  return normalized ? LOCAL_RUNNER_KIND_ALIASES[normalizeLookupKey(normalized)] : undefined;
318
533
  }
534
+ function normalizeGenerativeRunnerKind(value) {
535
+ const normalized = optionalText(value);
536
+ return normalized ? GENERATIVE_RUNNER_KIND_ALIASES[normalizeLookupKey(normalized)] : undefined;
537
+ }
319
538
  function normalizeLocalRunnerAuthMode(value) {
320
539
  const normalized = optionalText(value);
321
540
  return normalized ? LOCAL_RUNNER_AUTH_MODE_ALIASES[normalizeLookupKey(normalized)] : undefined;
@@ -332,11 +551,14 @@ function normalizeMcodaLocalRunnerConfig(adapter, config) {
332
551
  const merged = { ...localRunnerRecord, ...configRecord };
333
552
  const authMode = normalizeLocalRunnerAuthMode(merged.authMode) ?? "none";
334
553
  const dummyBearerToken = readConfigString(merged, ["dummyBearerToken", "dummyApiKey"]);
554
+ const generativeRunnerKind = normalizeGenerativeRunnerKind(merged.runnerKind ?? merged.runner_kind);
335
555
  return {
336
556
  baseUrl: readConfigString(merged, ["baseUrl", "base_url", "endpoint", "apiBaseUrl", "api_base_url"]),
337
557
  endpoint: readConfigString(merged, ["endpoint"]),
338
558
  apiBaseUrl: readConfigString(merged, ["apiBaseUrl", "api_base_url"]),
339
- runnerKind: normalizeLocalRunnerKind(merged.runnerKind) ?? defaultLocalRunnerKindForAdapter(adapter),
559
+ runnerKind: normalizeLocalRunnerKind(merged.runnerKind ?? merged.runner_kind) ??
560
+ (generativeRunnerKind ? "custom" : defaultLocalRunnerKindForAdapter(adapter)),
561
+ generativeRunnerKind,
340
562
  authMode,
341
563
  dummyBearerToken: authMode === "dummy-bearer" ? dummyBearerToken ?? "local" : dummyBearerToken,
342
564
  headers: normalizeStringMap(merged.headers),
@@ -348,7 +570,11 @@ function normalizeMcodaLocalRunnerConfig(adapter, config) {
348
570
  supportsStreaming: readConfigBoolean(merged, ["supportsStreaming", "supports_streaming"]),
349
571
  supportsTools: readConfigBoolean(merged, ["supportsTools", "supports_tools"]),
350
572
  supportsJsonSchema: readConfigBoolean(merged, ["supportsJsonSchema", "supports_json_schema"]),
351
- supportsGbnf: readConfigBoolean(merged, ["supportsGbnf", "supports_gbnf"])
573
+ supportsGbnf: readConfigBoolean(merged, ["supportsGbnf", "supports_gbnf"]),
574
+ inputModalities: normalizeGenerativeModalities(merged.inputModalities ?? merged.input_modalities),
575
+ outputModalities: normalizeGenerativeModalities(merged.outputModalities ?? merged.output_modalities),
576
+ operations: normalizeGenerativeOperations(merged.operations),
577
+ publicModelId: readConfigString(merged, ["publicModelId", "public_model_id"])
352
578
  };
353
579
  }
354
580
  function parsePositiveInteger(value, fallback) {
@@ -681,8 +907,23 @@ function buildCatalogFingerprint(models) {
681
907
  adapter: optionalText(model.adapter) || null,
682
908
  source_agent_slug: optionalText(model.source_agent_slug) || null,
683
909
  model_id: optionalText(model.model_id) || optionalText(model.model) || null,
910
+ public_model_id: optionalText(model.public_model_id) || null,
911
+ upstream_model: optionalText(model.upstream_model) || optionalText(model.model) || null,
684
912
  exposed: model.exposed !== false,
685
913
  capabilities: normalizeCapabilities(model.capabilities).sort(),
914
+ input_modalities: [...(model.input_modalities ?? [])].sort(),
915
+ output_modalities: [...(model.output_modalities ?? [])].sort(),
916
+ operations: (model.operations ?? [])
917
+ .map((operation) => ({
918
+ type: operation.type,
919
+ path: operation.path,
920
+ method: operation.method,
921
+ supported_parameters: [...operation.supported_parameters].sort(),
922
+ response_formats: [...(operation.response_formats ?? [])].sort(),
923
+ output_mime_types: [...(operation.output_mime_types ?? [])].sort(),
924
+ limits: operation.limits ?? null
925
+ }))
926
+ .sort((left, right) => left.type.localeCompare(right.type)),
686
927
  health_status: normalizeHealthStatus(model.health_status)
687
928
  }))
688
929
  .sort((left, right) => `${left.provider || ""}:${left.name}`.localeCompare(`${right.provider || ""}:${right.name}`));
@@ -860,6 +1101,69 @@ async function fetchJson(fetchImpl, url, init, timeoutMs) {
860
1101
  clearTimeout(timeout);
861
1102
  }
862
1103
  }
1104
+ function generativeOutputByteLimit(operation, configuredLimit) {
1105
+ if (typeof configuredLimit === "number" &&
1106
+ Number.isSafeInteger(configuredLimit) &&
1107
+ configuredLimit > 0) {
1108
+ return configuredLimit;
1109
+ }
1110
+ return operation === "images.generations"
1111
+ ? DEFAULT_IMAGE_GENERATIVE_OUTPUT_BYTES
1112
+ : operation === "audio.generations"
1113
+ ? DEFAULT_AUDIO_GENERATIVE_OUTPUT_BYTES
1114
+ : DEFAULT_VIDEO_GENERATIVE_OUTPUT_BYTES;
1115
+ }
1116
+ function generativeJsonResponseByteLimit(outputByteLimit) {
1117
+ const encodedLimit = Math.ceil(outputByteLimit / 3) * 4;
1118
+ return Math.min(Number.MAX_SAFE_INTEGER, encodedLimit + GENERATIVE_RESPONSE_JSON_OVERHEAD_BYTES);
1119
+ }
1120
+ async function readBoundedResponseText(response, maxBytes, overflowMessage) {
1121
+ const declaredLength = Number(response.headers.get("content-length"));
1122
+ if (Number.isFinite(declaredLength) &&
1123
+ declaredLength >= 0 &&
1124
+ declaredLength > maxBytes) {
1125
+ await response.body?.cancel().catch(() => undefined);
1126
+ throw new Error(overflowMessage);
1127
+ }
1128
+ if (!response.body) {
1129
+ return "";
1130
+ }
1131
+ const reader = response.body.getReader();
1132
+ const chunks = [];
1133
+ let totalBytes = 0;
1134
+ try {
1135
+ while (true) {
1136
+ const { done, value } = await reader.read();
1137
+ if (done)
1138
+ break;
1139
+ totalBytes += value.byteLength;
1140
+ if (totalBytes > maxBytes) {
1141
+ await reader.cancel().catch(() => undefined);
1142
+ throw new Error(overflowMessage);
1143
+ }
1144
+ chunks.push(Buffer.from(value));
1145
+ }
1146
+ }
1147
+ finally {
1148
+ reader.releaseLock();
1149
+ }
1150
+ return Buffer.concat(chunks, totalBytes).toString("utf8");
1151
+ }
1152
+ function base64DecodedByteLength(value) {
1153
+ if (value.length === 0 ||
1154
+ value.length % 4 !== 0 ||
1155
+ !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) {
1156
+ return null;
1157
+ }
1158
+ const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
1159
+ return (value.length / 4) * 3 - padding;
1160
+ }
1161
+ function sanitizeStableDiffusionCppExtraArgsTags(value) {
1162
+ return value
1163
+ .replace(STABLE_DIFFUSION_CPP_EXTRA_ARGS_TAG_PATTERN, "")
1164
+ .replace(STABLE_DIFFUSION_CPP_EXTRA_ARGS_TAG_FRAGMENT_PATTERN, "")
1165
+ .trim();
1166
+ }
863
1167
  export async function readSelfHostedNodeState(statePath) {
864
1168
  try {
865
1169
  const content = await readFile(statePath, "utf8");
@@ -1522,17 +1826,20 @@ export function mapOllamaModelToSelfHostedModel(model, config) {
1522
1826
  const parameterSize = optionalText(model.details?.parameter_size);
1523
1827
  const quantizationLevel = optionalText(model.details?.quantization_level);
1524
1828
  const embeddingOnly = isEmbeddingModel(name, family);
1829
+ const operations = [defaultGenerativeOperationDescriptor("chat.completions", "/api/chat")];
1830
+ const supportsVision = isVisionModel(name, family);
1525
1831
  return {
1526
1832
  name,
1527
1833
  provider: "ollama",
1528
1834
  adapter: "ollama",
1529
1835
  model_id: name,
1836
+ upstream_model: name,
1530
1837
  digest: optionalText(model.digest),
1531
1838
  family,
1532
1839
  parameter_size: parameterSize,
1533
1840
  quantization_level: quantizationLevel,
1534
1841
  supports_tools: false,
1535
- supports_vision: isVisionModel(name, family),
1842
+ supports_vision: supportsVision,
1536
1843
  openai_compatible: false,
1537
1844
  exposed: isModelExposed(name, family, config),
1538
1845
  best_usage: embeddingOnly
@@ -1544,7 +1851,10 @@ export function mapOllamaModelToSelfHostedModel(model, config) {
1544
1851
  reasoning_rating: 4,
1545
1852
  max_complexity: 3,
1546
1853
  health_status: "healthy",
1547
- metadata_quality: model.details ? "discovered" : "unknown"
1854
+ metadata_quality: model.details ? "discovered" : "unknown",
1855
+ input_modalities: supportsVision ? ["text", "image"] : ["text"],
1856
+ output_modalities: ["text"],
1857
+ operations: operations.map(generativeOperationCatalogInput)
1548
1858
  };
1549
1859
  }
1550
1860
  export function mapMcodaAgentToSelfHostedModel(agent, config) {
@@ -1568,6 +1878,17 @@ export function mapMcodaAgentToSelfHostedModel(agent, config) {
1568
1878
  const supportsTools = optionalBoolean(agent.supportsTools, agent.supports_tools, localRunner?.supportsTools) === true;
1569
1879
  const openaiCompatible = localRunner !== undefined || optionalBoolean(agent.openaiCompatible, agent.openai_compatible) === true;
1570
1880
  const model = defaultModel || slug;
1881
+ const operations = localRunner?.operations ??
1882
+ [defaultGenerativeOperationDescriptor("chat.completions", localRunner ? "/chat/completions" : undefined)];
1883
+ const inferredInputModalities = inferGenerativeModalities(operations, "input");
1884
+ const supportsVision = capabilities.some((capability) => capability.toLowerCase().includes("vision")) ||
1885
+ capabilities.some((capability) => capability.toLowerCase().includes("visual"));
1886
+ const inputModalities = localRunner?.inputModalities ??
1887
+ (supportsVision && operations.some((entry) => entry.operation === "chat.completions")
1888
+ ? Array.from(new Set([...inferredInputModalities, "image"]))
1889
+ : inferredInputModalities);
1890
+ const outputModalities = localRunner?.outputModalities ?? inferGenerativeModalities(operations, "output");
1891
+ const publicModelId = localRunner?.publicModelId;
1571
1892
  return {
1572
1893
  name: slug,
1573
1894
  provider: "mcoda",
@@ -1575,9 +1896,11 @@ export function mapMcodaAgentToSelfHostedModel(agent, config) {
1575
1896
  source_agent_id: optionalText(agent.id),
1576
1897
  source_agent_slug: slug,
1577
1898
  model,
1578
- model_id: model,
1899
+ model_id: publicModelId || model,
1900
+ public_model_id: publicModelId,
1901
+ upstream_model: model,
1579
1902
  base_url: localRunner?.baseUrl,
1580
- runner_kind: localRunner?.runnerKind,
1903
+ runner_kind: localRunner?.generativeRunnerKind ?? localRunner?.runnerKind,
1581
1904
  auth_mode: localRunner?.authMode,
1582
1905
  response_format_strategy: localRunner?.responseFormatStrategy,
1583
1906
  health_path: localRunner?.healthPath,
@@ -1587,8 +1910,7 @@ export function mapMcodaAgentToSelfHostedModel(agent, config) {
1587
1910
  max_output_tokens: optionalNumber(agent.maxOutputTokens, agent.max_output_tokens),
1588
1911
  supports_tools: supportsTools,
1589
1912
  supports_streaming: localRunner?.supportsStreaming,
1590
- supports_vision: capabilities.some((capability) => capability.toLowerCase().includes("vision")) ||
1591
- capabilities.some((capability) => capability.toLowerCase().includes("visual")),
1913
+ supports_vision: supportsVision,
1592
1914
  supports_json_schema: localRunner?.supportsJsonSchema,
1593
1915
  supports_gbnf: localRunner?.supportsGbnf,
1594
1916
  openai_compatible: openaiCompatible,
@@ -1601,7 +1923,10 @@ export function mapMcodaAgentToSelfHostedModel(agent, config) {
1601
1923
  reasoning_rating: optionalNumber(agent.reasoningRating, agent.reasoning_rating),
1602
1924
  max_complexity: optionalNumber(agent.maxComplexity, agent.max_complexity),
1603
1925
  health_status: healthStatus,
1604
- metadata_quality: "discovered"
1926
+ metadata_quality: "discovered",
1927
+ input_modalities: inputModalities,
1928
+ output_modalities: outputModalities,
1929
+ operations: operations.map(generativeOperationCatalogInput)
1605
1930
  };
1606
1931
  }
1607
1932
  function parseMcodaAgentListOutput(stdout) {
@@ -1905,6 +2230,8 @@ function mapMcodaAgentToCodaliAgent(agent, fallbackSlug) {
1905
2230
  const localRunner = normalizeMcodaLocalRunnerConfig(adapter, config);
1906
2231
  const configAuthMode = normalizeLocalRunnerAuthMode(config?.authMode);
1907
2232
  const supportsTools = optionalBoolean(agent.supportsTools, agent.supports_tools, localRunner?.supportsTools) === true;
2233
+ const generativeOperations = localRunner?.operations ??
2234
+ [defaultGenerativeOperationDescriptor("chat.completions", localRunner ? "/chat/completions" : undefined)];
1908
2235
  return {
1909
2236
  slug: optionalText(agent.slug) || fallbackSlug,
1910
2237
  adapter,
@@ -1918,6 +2245,7 @@ function mapMcodaAgentToCodaliAgent(agent, fallbackSlug) {
1918
2245
  : configText(config, "apiKey", "api_key"),
1919
2246
  localRunner,
1920
2247
  runnerKind: localRunner?.runnerKind,
2248
+ generativeRunnerKind: localRunner?.generativeRunnerKind,
1921
2249
  authMode: localRunner?.authMode ?? configAuthMode,
1922
2250
  dummyBearerToken: localRunner?.dummyBearerToken,
1923
2251
  headers: localRunner?.headers,
@@ -1933,6 +2261,10 @@ function mapMcodaAgentToCodaliAgent(agent, fallbackSlug) {
1933
2261
  capabilities: normalizeCapabilities(agent.capabilities),
1934
2262
  contextWindow: optionalNumber(agent.contextWindow, agent.context_window) ?? undefined,
1935
2263
  maxOutputTokens: optionalNumber(agent.maxOutputTokens, agent.max_output_tokens) ?? undefined,
2264
+ generativeOperations,
2265
+ inputModalities: localRunner?.inputModalities ?? inferGenerativeModalities(generativeOperations, "input"),
2266
+ outputModalities: localRunner?.outputModalities ?? inferGenerativeModalities(generativeOperations, "output"),
2267
+ publicModelId: localRunner?.publicModelId
1936
2268
  };
1937
2269
  }
1938
2270
  function buildCodaliWorkspace(job) {
@@ -1958,6 +2290,7 @@ const PRE_START_JOB_ERROR_CODES = new Set([
1958
2290
  "selected_agent_unavailable",
1959
2291
  "selected_agent_auth_unavailable",
1960
2292
  "selected_agent_unhealthy",
2293
+ "unsupported_operation",
1961
2294
  "validation_failed",
1962
2295
  "docdex_context_missing",
1963
2296
  "docdex_api_key_missing",
@@ -2148,7 +2481,7 @@ function buildCodaliJob(job) {
2148
2481
  return {
2149
2482
  id: optionalText(payload.id) || job.job_id,
2150
2483
  jobType,
2151
- input: payload.input ?? messagesToPrompt(job.openai_request.messages),
2484
+ input: payload.input ?? messagesToPrompt(job.openai_request.messages ?? []),
2152
2485
  context: objectRecord(payload.context) || undefined,
2153
2486
  tenant: objectRecord(payload.tenant) || undefined,
2154
2487
  requester: objectRecord(payload.requester) || undefined,
@@ -2182,7 +2515,7 @@ function buildCodaliPolicy(job) {
2182
2515
  }
2183
2516
  function normalizeGatewayConversation(job, value) {
2184
2517
  const record = objectRecord(value);
2185
- const rawMessages = Array.isArray(record?.messages) ? record.messages : job.openai_request.messages;
2518
+ const rawMessages = Array.isArray(record?.messages) ? record.messages : (job.openai_request.messages ?? []);
2186
2519
  const messages = rawMessages
2187
2520
  .map((entry) => {
2188
2521
  const message = objectRecord(entry);
@@ -2279,7 +2612,7 @@ function buildCodaliGateway(job) {
2279
2612
  const payload = job.codali_gateway;
2280
2613
  if (!payload)
2281
2614
  return undefined;
2282
- const query = optionalText(payload.query) || messagesToPrompt(job.openai_request.messages);
2615
+ const query = optionalText(payload.query) || messagesToPrompt(job.openai_request.messages ?? []);
2283
2616
  return {
2284
2617
  id: optionalText(payload.id) || job.job_id,
2285
2618
  query,
@@ -3903,6 +4236,7 @@ export class SelfHostedNodeRuntime {
3903
4236
  this.recentFailures = [];
3904
4237
  this.lifecyclePollingDisabled = false;
3905
4238
  this.config = config;
4239
+ this.fetchImpl = deps?.fetchImpl || fetch;
3906
4240
  this.gateway =
3907
4241
  deps?.gateway ||
3908
4242
  new MswarmSelfHostedNodeClient({
@@ -3911,7 +4245,7 @@ export class SelfHostedNodeRuntime {
3911
4245
  jobsStartPathTemplate: config.jobsStartPathTemplate,
3912
4246
  jobsEventsPathTemplate: config.jobsEventsPathTemplate,
3913
4247
  jobsResultPathTemplate: config.jobsResultPathTemplate,
3914
- fetchImpl: deps?.fetchImpl,
4248
+ fetchImpl: this.fetchImpl,
3915
4249
  timeoutMs: config.requestTimeoutMs
3916
4250
  });
3917
4251
  this.mcoda =
@@ -3933,14 +4267,14 @@ export class SelfHostedNodeRuntime {
3933
4267
  deps?.ollama ||
3934
4268
  new OllamaClient({
3935
4269
  baseUrl: config.ollamaBaseUrl,
3936
- fetchImpl: deps?.fetchImpl,
4270
+ fetchImpl: this.fetchImpl,
3937
4271
  timeoutMs: config.requestTimeoutMs
3938
4272
  });
3939
4273
  this.jobOllama =
3940
4274
  deps?.ollama ||
3941
4275
  new OllamaClient({
3942
4276
  baseUrl: config.ollamaBaseUrl,
3943
- fetchImpl: deps?.fetchImpl,
4277
+ fetchImpl: this.fetchImpl,
3944
4278
  timeoutMs: config.jobTimeoutMs
3945
4279
  });
3946
4280
  this.capabilityRunner = deps?.capabilityRunner || defaultCommandRunner;
@@ -4025,11 +4359,16 @@ export class SelfHostedNodeRuntime {
4025
4359
  });
4026
4360
  }
4027
4361
  jobResultPayload(result) {
4362
+ const operation = normalizeSelfHostedGenerativeOperationName(result.operation) ?? "chat.completions";
4028
4363
  return {
4029
4364
  ...result,
4030
4365
  node_id: this.config.nodeId,
4031
4366
  ...(result.status === "success"
4032
- ? { usage: result.usage || openAiUsage(result.openai_response) }
4367
+ ? result.usage
4368
+ ? { usage: result.usage }
4369
+ : operation === "chat.completions"
4370
+ ? { usage: openAiUsage(result.openai_response) }
4371
+ : {}
4033
4372
  : {})
4034
4373
  };
4035
4374
  }
@@ -4397,7 +4736,7 @@ export class SelfHostedNodeRuntime {
4397
4736
  }
4398
4737
  const body = {
4399
4738
  model: optionalText(agent.model) || job.openai_request.model,
4400
- messages: job.openai_request.messages,
4739
+ messages: job.openai_request.messages ?? [],
4401
4740
  tools: job.openai_request.tools,
4402
4741
  stream: false
4403
4742
  };
@@ -4412,7 +4751,7 @@ export class SelfHostedNodeRuntime {
4412
4751
  if (job.openai_request.stop !== undefined)
4413
4752
  body.stop = job.openai_request.stop;
4414
4753
  const url = `${baseUrl.replace(/\/+$/, "")}/chat/completions`;
4415
- const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
4754
+ const response = await this.fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body) });
4416
4755
  const text = await response.text();
4417
4756
  let payload = null;
4418
4757
  try {
@@ -4426,6 +4765,335 @@ export class SelfHostedNodeRuntime {
4426
4765
  }
4427
4766
  return payload;
4428
4767
  }
4768
+ generativeOperationForAgent(agent, operation) {
4769
+ const descriptor = agent.generativeOperations.find((entry) => entry.operation === operation);
4770
+ if (!descriptor) {
4771
+ throw new SelfHostedPreStartJobError("unsupported_operation", `selected local mcoda agent ${agent.slug} does not expose ${operation}`);
4772
+ }
4773
+ return descriptor;
4774
+ }
4775
+ prepareOpenAiGenerativePassthrough(job, agent, operation) {
4776
+ const descriptor = this.generativeOperationForAgent(agent, operation);
4777
+ if (job.openai_request.stream === true) {
4778
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} does not support streaming through the self-hosted relay`);
4779
+ }
4780
+ const baseUrl = optionalText(agent.baseUrl);
4781
+ if (!baseUrl) {
4782
+ throw new SelfHostedPreStartJobError("selected_agent_unavailable", `agent has no local runner base URL for ${operation}`);
4783
+ }
4784
+ let parsedBaseUrl;
4785
+ try {
4786
+ parsedBaseUrl = new URL(baseUrl);
4787
+ }
4788
+ catch {
4789
+ throw new SelfHostedPreStartJobError("selected_agent_unavailable", `agent has an invalid local runner base URL for ${operation}`);
4790
+ }
4791
+ if ((parsedBaseUrl.protocol !== "http:" && parsedBaseUrl.protocol !== "https:") ||
4792
+ parsedBaseUrl.username ||
4793
+ parsedBaseUrl.password ||
4794
+ parsedBaseUrl.search ||
4795
+ parsedBaseUrl.hash) {
4796
+ throw new SelfHostedPreStartJobError("selected_agent_unavailable", `agent local runner base URL is not valid for ${operation}`);
4797
+ }
4798
+ const prompt = optionalText(job.openai_request.prompt);
4799
+ if (!prompt) {
4800
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} requires a non-empty prompt`);
4801
+ }
4802
+ const rawResponseFormat = job.openai_request.response_format;
4803
+ if (rawResponseFormat !== undefined && rawResponseFormat !== null && typeof rawResponseFormat !== "string") {
4804
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} response_format must be a string`);
4805
+ }
4806
+ const responseFormats = descriptor.responseFormats?.map((entry) => entry.toLowerCase()) ??
4807
+ (operation === "images.generations"
4808
+ ? ["b64_json"]
4809
+ : operation === "audio.generations"
4810
+ ? ["wav", "flac", "mp3", "ogg"]
4811
+ : ["webm", "webp", "avi"]);
4812
+ const requestedResponseFormat = (optionalText(rawResponseFormat) ||
4813
+ (operation === "images.generations"
4814
+ ? "b64_json"
4815
+ : operation === "audio.generations"
4816
+ ? responseFormats[0] || "wav"
4817
+ : responseFormats[0] || "webm")).toLowerCase();
4818
+ if (!responseFormats.includes(requestedResponseFormat) ||
4819
+ (operation === "images.generations"
4820
+ ? requestedResponseFormat !== "b64_json"
4821
+ : operation === "audio.generations"
4822
+ ? !AUDIO_GENERATION_RESPONSE_FORMATS.has(requestedResponseFormat)
4823
+ : !VIDEO_GENERATION_RESPONSE_FORMATS.has(requestedResponseFormat))) {
4824
+ throw new SelfHostedPreStartJobError("validation_failed", operation === "images.generations"
4825
+ ? `${operation} requires response_format b64_json through the self-hosted relay`
4826
+ : operation === "audio.generations"
4827
+ ? `${operation} response_format must be one of wav, flac, mp3, or ogg`
4828
+ : `${operation} response_format must be one of webm, webp, or avi`);
4829
+ }
4830
+ const body = {};
4831
+ for (const parameter of descriptor.requestParameterAllowlist) {
4832
+ if (parameter === "model") {
4833
+ body.model = optionalText(agent.model) || job.openai_request.model;
4834
+ }
4835
+ else if (parameter === "response_format") {
4836
+ body.response_format = requestedResponseFormat;
4837
+ }
4838
+ else if (job.openai_request[parameter] !== undefined) {
4839
+ body[parameter] = job.openai_request[parameter];
4840
+ }
4841
+ }
4842
+ body.model = optionalText(agent.model) || job.openai_request.model;
4843
+ body.prompt = prompt;
4844
+ body.response_format = requestedResponseFormat;
4845
+ const n = body.n;
4846
+ if (n !== undefined && (!Number.isSafeInteger(n) || n <= 0)) {
4847
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} n must be a positive integer`);
4848
+ }
4849
+ const durationSeconds = body.duration_seconds;
4850
+ if (durationSeconds !== undefined &&
4851
+ (typeof durationSeconds !== "number" || !Number.isFinite(durationSeconds) || durationSeconds <= 0)) {
4852
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} duration_seconds must be a positive number`);
4853
+ }
4854
+ const sampleRate = body.sample_rate;
4855
+ if (sampleRate !== undefined &&
4856
+ (!Number.isSafeInteger(sampleRate) || sampleRate <= 0)) {
4857
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} sample_rate must be a positive integer`);
4858
+ }
4859
+ const fps = body.fps;
4860
+ if (fps !== undefined && (!Number.isSafeInteger(fps) || fps <= 0)) {
4861
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} fps must be a positive integer`);
4862
+ }
4863
+ const videoFrames = body.video_frames;
4864
+ if (videoFrames !== undefined &&
4865
+ (!Number.isSafeInteger(videoFrames) || videoFrames <= 0)) {
4866
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} video_frames must be a positive integer`);
4867
+ }
4868
+ if (operation === "videos.generations" &&
4869
+ durationSeconds !== undefined &&
4870
+ videoFrames !== undefined) {
4871
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} accepts duration_seconds or video_frames, not both`);
4872
+ }
4873
+ const steps = body.steps;
4874
+ if (steps !== undefined && (!Number.isSafeInteger(steps) || steps <= 0)) {
4875
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} steps must be a positive integer`);
4876
+ }
4877
+ const highNoiseSteps = body.high_noise_steps;
4878
+ if (highNoiseSteps !== undefined &&
4879
+ (!Number.isSafeInteger(highNoiseSteps) || highNoiseSteps <= 0)) {
4880
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} high_noise_steps must be a positive integer`);
4881
+ }
4882
+ const seed = body.seed;
4883
+ if (seed !== undefined && !Number.isSafeInteger(seed)) {
4884
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} seed must be an integer`);
4885
+ }
4886
+ if (body.size !== undefined && typeof body.size !== "string") {
4887
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} size must be a string`);
4888
+ }
4889
+ if (body.negative_prompt !== undefined && typeof body.negative_prompt !== "string") {
4890
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} negative_prompt must be a string`);
4891
+ }
4892
+ const limits = descriptor.limits;
4893
+ if (limits?.maxPromptChars && prompt.length > limits.maxPromptChars) {
4894
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} prompt exceeds the configured limit`);
4895
+ }
4896
+ if (limits?.maxNegativePromptChars &&
4897
+ typeof body.negative_prompt === "string" &&
4898
+ body.negative_prompt.length > limits.maxNegativePromptChars) {
4899
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} negative_prompt exceeds the configured limit`);
4900
+ }
4901
+ if (limits?.maxN && typeof n === "number" && n > limits.maxN) {
4902
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} n exceeds the configured limit`);
4903
+ }
4904
+ if (limits?.minDurationSeconds &&
4905
+ typeof durationSeconds === "number" &&
4906
+ durationSeconds < limits.minDurationSeconds) {
4907
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} duration_seconds is below the configured minimum`);
4908
+ }
4909
+ if (limits?.maxDurationSeconds &&
4910
+ typeof durationSeconds === "number" &&
4911
+ durationSeconds > limits.maxDurationSeconds) {
4912
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} duration_seconds exceeds the configured limit`);
4913
+ }
4914
+ if (limits?.maxSampleRate &&
4915
+ typeof sampleRate === "number" &&
4916
+ sampleRate > limits.maxSampleRate) {
4917
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} sample_rate exceeds the configured limit`);
4918
+ }
4919
+ if (limits?.minFps && typeof fps === "number" && fps < limits.minFps) {
4920
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} fps is below the configured minimum`);
4921
+ }
4922
+ if (limits?.maxFps && typeof fps === "number" && fps > limits.maxFps) {
4923
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} fps exceeds the configured limit`);
4924
+ }
4925
+ if (limits?.minVideoFrames &&
4926
+ typeof videoFrames === "number" &&
4927
+ videoFrames < limits.minVideoFrames) {
4928
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} video_frames is below the configured minimum`);
4929
+ }
4930
+ if (limits?.maxVideoFrames &&
4931
+ typeof videoFrames === "number" &&
4932
+ videoFrames > limits.maxVideoFrames) {
4933
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} video_frames exceeds the configured limit`);
4934
+ }
4935
+ if (limits?.maxSteps && typeof steps === "number" && steps > limits.maxSteps) {
4936
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} steps exceeds the configured limit`);
4937
+ }
4938
+ if (limits?.maxHighNoiseSteps &&
4939
+ typeof highNoiseSteps === "number" &&
4940
+ highNoiseSteps > limits.maxHighNoiseSteps) {
4941
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} high_noise_steps exceeds the configured limit`);
4942
+ }
4943
+ if (typeof body.size === "string") {
4944
+ const sizeMatch = /^([1-9][0-9]*)x([1-9][0-9]*)$/.exec(body.size);
4945
+ if (!sizeMatch) {
4946
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} size must use WIDTHxHEIGHT`);
4947
+ }
4948
+ const width = Number(sizeMatch[1]);
4949
+ const height = Number(sizeMatch[2]);
4950
+ if ((limits?.minWidth && width < limits.minWidth) ||
4951
+ (limits?.minHeight && height < limits.minHeight)) {
4952
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} size is below the configured minimum`);
4953
+ }
4954
+ if ((limits?.maxWidth && width > limits.maxWidth) ||
4955
+ (limits?.maxHeight && height > limits.maxHeight) ||
4956
+ (limits?.maxPixels && width * height > limits.maxPixels)) {
4957
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} size exceeds the configured limit`);
4958
+ }
4959
+ }
4960
+ if (operation === "images.generations" &&
4961
+ agent.generativeRunnerKind === "stable-diffusion-cpp") {
4962
+ const sanitizedPrompt = sanitizeStableDiffusionCppExtraArgsTags(prompt);
4963
+ if (!sanitizedPrompt) {
4964
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} prompt is empty after removing reserved stable-diffusion.cpp tags`);
4965
+ }
4966
+ const controlledArgs = {};
4967
+ if (typeof body.negative_prompt === "string") {
4968
+ controlledArgs.negative_prompt =
4969
+ sanitizeStableDiffusionCppExtraArgsTags(body.negative_prompt);
4970
+ }
4971
+ if (typeof seed === "number") {
4972
+ controlledArgs.seed = seed;
4973
+ }
4974
+ if (typeof steps === "number") {
4975
+ controlledArgs.sample_params = { sample_steps: steps };
4976
+ }
4977
+ body.prompt =
4978
+ Object.keys(controlledArgs).length > 0
4979
+ ? `${sanitizedPrompt}\n<sd_cpp_extra_args>${JSON.stringify(controlledArgs)}</sd_cpp_extra_args>`
4980
+ : sanitizedPrompt;
4981
+ delete body.negative_prompt;
4982
+ delete body.seed;
4983
+ delete body.steps;
4984
+ }
4985
+ const serializedBody = JSON.stringify(body);
4986
+ if (limits?.maxRequestBytes && Buffer.byteLength(serializedBody, "utf8") > limits.maxRequestBytes) {
4987
+ throw new SelfHostedPreStartJobError("validation_failed", `${operation} request exceeds the configured byte limit`);
4988
+ }
4989
+ const headers = {};
4990
+ const bearer = agent.authMode === "bearer"
4991
+ ? optionalText(agent.apiKey)
4992
+ : agent.authMode === "dummy-bearer"
4993
+ ? optionalText(agent.dummyBearerToken) || "local"
4994
+ : undefined;
4995
+ if (bearer) {
4996
+ headers.authorization = `Bearer ${bearer}`;
4997
+ }
4998
+ for (const [key, value] of Object.entries(agent.headers ?? {})) {
4999
+ if (typeof value === "string") {
5000
+ headers[key.toLowerCase()] = value;
5001
+ }
5002
+ }
5003
+ headers["content-type"] = "application/json";
5004
+ return {
5005
+ descriptor,
5006
+ url: `${baseUrl.replace(/\/+$/, "")}${descriptor.path}`,
5007
+ headers,
5008
+ body
5009
+ };
5010
+ }
5011
+ async executeOpenAiGenerativePassthrough(prepared, operation) {
5012
+ const outputByteLimit = generativeOutputByteLimit(operation, prepared.descriptor.limits?.maxOutputBytes);
5013
+ const jsonResponseByteLimit = generativeJsonResponseByteLimit(outputByteLimit);
5014
+ const controller = new AbortController();
5015
+ const timeout = setTimeout(() => controller.abort(), this.config.jobTimeoutMs);
5016
+ let payload;
5017
+ try {
5018
+ const response = await this.fetchImpl(prepared.url, {
5019
+ method: "POST",
5020
+ headers: prepared.headers,
5021
+ body: JSON.stringify(prepared.body),
5022
+ signal: controller.signal
5023
+ });
5024
+ if (!response.ok) {
5025
+ await response.body?.cancel().catch(() => undefined);
5026
+ throw new Error(`local ${operation} runner request failed with status ${response.status}`);
5027
+ }
5028
+ const responseText = await readBoundedResponseText(response, jsonResponseByteLimit, `local ${operation} runner response exceeds the configured byte limit`);
5029
+ try {
5030
+ payload = JSON.parse(responseText);
5031
+ }
5032
+ catch {
5033
+ throw new Error(`local ${operation} runner returned invalid JSON`);
5034
+ }
5035
+ }
5036
+ catch (error) {
5037
+ if (error instanceof Error && error.name === "AbortError") {
5038
+ throw new Error(`local ${operation} runner timeout`);
5039
+ }
5040
+ throw error;
5041
+ }
5042
+ finally {
5043
+ clearTimeout(timeout);
5044
+ }
5045
+ if (!isRecord(payload) || !Array.isArray(payload.data) || payload.data.length === 0) {
5046
+ throw new Error(`local ${operation} runner returned an invalid base64 response`);
5047
+ }
5048
+ const limits = prepared.descriptor.limits;
5049
+ if (limits?.maxN && payload.data.length > limits.maxN) {
5050
+ throw new Error(`local ${operation} runner returned too many outputs`);
5051
+ }
5052
+ let decodedOutputBytes = 0;
5053
+ const normalizedData = [];
5054
+ for (const item of payload.data) {
5055
+ const encodedPayload = isRecord(item) && operation === "audio.generations"
5056
+ ? optionalText(item.b64_audio) || optionalText(item.b64_json)
5057
+ : isRecord(item) && operation === "videos.generations"
5058
+ ? optionalText(item.b64_video) || optionalText(item.b64_json)
5059
+ : isRecord(item)
5060
+ ? optionalText(item.b64_json)
5061
+ : null;
5062
+ if (!isRecord(item) || !encodedPayload) {
5063
+ throw new Error(`local ${operation} runner returned an invalid base64 response`);
5064
+ }
5065
+ const decodedBytes = base64DecodedByteLength(encodedPayload);
5066
+ if (decodedBytes === null) {
5067
+ throw new Error(`local ${operation} runner returned an invalid base64 response`);
5068
+ }
5069
+ decodedOutputBytes += decodedBytes;
5070
+ if (decodedOutputBytes > outputByteLimit) {
5071
+ throw new Error(`local ${operation} runner response exceeds the configured byte limit`);
5072
+ }
5073
+ const mimeType = optionalText(item.mime_type);
5074
+ if (mimeType &&
5075
+ prepared.descriptor.outputMimeTypes?.length &&
5076
+ !prepared.descriptor.outputMimeTypes.includes(mimeType.toLowerCase())) {
5077
+ throw new Error(`local ${operation} runner returned unsupported mime_type ${mimeType}`);
5078
+ }
5079
+ if ((operation === "audio.generations" || operation === "videos.generations") &&
5080
+ !optionalText(operation === "audio.generations" ? item.b64_audio : item.b64_video) &&
5081
+ optionalText(item.b64_json)) {
5082
+ const normalizedItem = {
5083
+ ...item,
5084
+ [operation === "audio.generations" ? "b64_audio" : "b64_video"]: encodedPayload
5085
+ };
5086
+ delete normalizedItem.b64_json;
5087
+ normalizedData.push(normalizedItem);
5088
+ }
5089
+ else {
5090
+ normalizedData.push(item);
5091
+ }
5092
+ }
5093
+ return operation === "audio.generations" || operation === "videos.generations"
5094
+ ? { ...payload, data: normalizedData }
5095
+ : payload;
5096
+ }
4429
5097
  async resolveMcodaAgentForJob(job) {
4430
5098
  const selectedSourceAgentSlug = optionalText(job.source_agent_slug);
4431
5099
  const selectedAgentSlug = optionalText(job.agent_slug);
@@ -4647,6 +5315,7 @@ export class SelfHostedNodeRuntime {
4647
5315
  const startedAt = Date.now();
4648
5316
  this.beginExecutionTelemetry("llm");
4649
5317
  let selectedAgent;
5318
+ let operation = "chat.completions";
4650
5319
  let jobStarted = false;
4651
5320
  const progressEvents = [];
4652
5321
  const streamEvents = [];
@@ -4676,6 +5345,9 @@ export class SelfHostedNodeRuntime {
4676
5345
  const result = {
4677
5346
  job_id: job.job_id,
4678
5347
  request_id: job.request_id,
5348
+ ...(normalizeSelfHostedGenerativeOperationName(job.operation)
5349
+ ? { operation: normalizeSelfHostedGenerativeOperationName(job.operation) }
5350
+ : {}),
4679
5351
  status: "failed",
4680
5352
  pre_start_failure: true,
4681
5353
  error: { code: "validation_failed", message: "job node_id does not match this node" }
@@ -4689,7 +5361,20 @@ export class SelfHostedNodeRuntime {
4689
5361
  return result;
4690
5362
  }
4691
5363
  try {
5364
+ try {
5365
+ operation = resolveSelfHostedInvocationOperation(job.operation);
5366
+ }
5367
+ catch {
5368
+ throw new SelfHostedPreStartJobError("unsupported_operation", `unsupported self-hosted operation ${optionalText(job.operation) || String(job.operation)}`);
5369
+ }
4692
5370
  if (job.provider === "ollama") {
5371
+ if (operation !== "chat.completions") {
5372
+ throw new SelfHostedPreStartJobError("unsupported_operation", `Ollama relay jobs do not support ${operation}`);
5373
+ }
5374
+ const chatMessages = job.openai_request.messages;
5375
+ if (!Array.isArray(chatMessages)) {
5376
+ throw new SelfHostedPreStartJobError("validation_failed", "chat.completions requires messages");
5377
+ }
4693
5378
  const options = {};
4694
5379
  if (job.openai_request.temperature !== undefined)
4695
5380
  options.temperature = job.openai_request.temperature;
@@ -4702,7 +5387,7 @@ export class SelfHostedNodeRuntime {
4702
5387
  await acknowledgeStarted();
4703
5388
  const ollamaResult = await this.jobOllama.chat({
4704
5389
  model: job.model || job.openai_request.model,
4705
- messages: job.openai_request.messages,
5390
+ messages: chatMessages,
4706
5391
  options,
4707
5392
  format: resolveOllamaResponseFormat(job.openai_request.response_format)
4708
5393
  });
@@ -4731,6 +5416,7 @@ export class SelfHostedNodeRuntime {
4731
5416
  const invocationResult = {
4732
5417
  job_id: job.job_id,
4733
5418
  request_id: job.request_id,
5419
+ operation,
4734
5420
  status: "success",
4735
5421
  openai_response: buildOpenAIChatCompletion({
4736
5422
  requestId: job.request_id,
@@ -4752,12 +5438,43 @@ export class SelfHostedNodeRuntime {
4752
5438
  });
4753
5439
  return invocationResult;
4754
5440
  }
4755
- const taskPreview = messagesToPrompt(job.openai_request.messages);
4756
- if (!taskPreview) {
4757
- throw new Error("mcoda invocation prompt is empty");
4758
- }
4759
5441
  const agent = await this.resolveMcodaAgentForJob(job);
4760
5442
  selectedAgent = agent;
5443
+ this.generativeOperationForAgent(agent, operation);
5444
+ if (operation === "images.generations" ||
5445
+ operation === "audio.generations" ||
5446
+ operation === "videos.generations") {
5447
+ const prepared = this.prepareOpenAiGenerativePassthrough(job, agent, operation);
5448
+ await acknowledgeStarted(agent);
5449
+ await recordProgress({
5450
+ type: "agent_selected",
5451
+ operation,
5452
+ job_id: job.job_id,
5453
+ request_id: job.request_id,
5454
+ agent_slug: agent.slug,
5455
+ adapter: agent.adapter,
5456
+ local_model: optionalText(agent.model) || job.openai_request.model
5457
+ });
5458
+ const passthrough = await this.executeOpenAiGenerativePassthrough(prepared, operation);
5459
+ this.finishExecutionTelemetry({ executionClass: "llm", startedAt, ok: true });
5460
+ return {
5461
+ job_id: job.job_id,
5462
+ request_id: job.request_id,
5463
+ operation,
5464
+ status: "success",
5465
+ openai_response: passthrough,
5466
+ ...(progressEvents.length ? { progress_events: progressEvents } : {}),
5467
+ timing: { local_latency_ms: Date.now() - startedAt }
5468
+ };
5469
+ }
5470
+ const chatMessages = job.openai_request.messages;
5471
+ if (!Array.isArray(chatMessages)) {
5472
+ throw new SelfHostedPreStartJobError("validation_failed", "chat.completions requires messages");
5473
+ }
5474
+ const taskPreview = messagesToPrompt(chatMessages);
5475
+ if (!taskPreview) {
5476
+ throw new SelfHostedPreStartJobError("validation_failed", "mcoda invocation prompt is empty");
5477
+ }
4761
5478
  validateRequiredDocdexContext(job, options.attachedMswarmApiKey);
4762
5479
  const attachedMswarmApiKey = attachedMswarmApiKeyForDocdex(job, options.attachedMswarmApiKey);
4763
5480
  await acknowledgeStarted(agent);
@@ -4779,6 +5496,7 @@ export class SelfHostedNodeRuntime {
4779
5496
  const passthroughResult = {
4780
5497
  job_id: job.job_id,
4781
5498
  request_id: job.request_id,
5499
+ operation,
4782
5500
  status: "success",
4783
5501
  openai_response: passthrough,
4784
5502
  ...(progressEvents.length ? { progress_events: progressEvents } : {}),
@@ -4793,14 +5511,16 @@ export class SelfHostedNodeRuntime {
4793
5511
  jobId: job.job_id,
4794
5512
  requestId: job.request_id,
4795
5513
  model: job.openai_request.model,
4796
- messages: job.openai_request.messages,
5514
+ messages: chatMessages,
4797
5515
  agent,
4798
5516
  workspace: buildCodaliWorkspace(job),
4799
5517
  docdex: buildCodaliDocdex(job),
4800
5518
  attachedMswarmApiKey,
4801
5519
  policy: buildCodaliPolicy(job),
4802
5520
  temperature: job.openai_request.temperature,
4803
- responseFormat: job.openai_request.response_format ?? null,
5521
+ responseFormat: isRecord(job.openai_request.response_format)
5522
+ ? job.openai_request.response_format
5523
+ : null,
4804
5524
  stream: job.openai_request.stream === true,
4805
5525
  codaliGateway,
4806
5526
  codaliJob,
@@ -4860,6 +5580,7 @@ export class SelfHostedNodeRuntime {
4860
5580
  const result = {
4861
5581
  job_id: job.job_id,
4862
5582
  request_id: job.request_id,
5583
+ operation,
4863
5584
  status: "success",
4864
5585
  openai_response: buildOpenAIChatCompletion({
4865
5586
  requestId: job.request_id,
@@ -4932,6 +5653,7 @@ export class SelfHostedNodeRuntime {
4932
5653
  const result = {
4933
5654
  job_id: job.job_id,
4934
5655
  request_id: job.request_id,
5656
+ operation,
4935
5657
  status: "failed",
4936
5658
  ...(!jobStarted ? { pre_start_failure: true } : {}),
4937
5659
  error: {
@@ -5147,6 +5869,9 @@ export class SelfHostedNodeRuntime {
5147
5869
  if (!job) {
5148
5870
  return { executed: false };
5149
5871
  }
5872
+ const chatStreaming = (normalizeSelfHostedGenerativeOperationName(job.operation) ?? "chat.completions") ===
5873
+ "chat.completions" &&
5874
+ job.openai_request.stream === true;
5150
5875
  const pendingStreamEvents = [];
5151
5876
  let streamEventForwardingFailed = false;
5152
5877
  let forwardedStreamEventCount = 0;
@@ -5193,7 +5918,7 @@ export class SelfHostedNodeRuntime {
5193
5918
  }
5194
5919
  },
5195
5920
  onOpenAIChunk: async (chunk) => {
5196
- if (job.openai_request.stream !== true || streamEventForwardingFailed) {
5921
+ if (!chatStreaming || streamEventForwardingFailed) {
5197
5922
  return;
5198
5923
  }
5199
5924
  pendingStreamEvents.push(chunk);
@@ -5212,6 +5937,7 @@ export class SelfHostedNodeRuntime {
5212
5937
  result = {
5213
5938
  job_id: job.job_id,
5214
5939
  request_id: job.request_id,
5940
+ operation: result.operation,
5215
5941
  status: "failed",
5216
5942
  error: { code: SELF_HOSTED_PROTOCOL_MISMATCH_CODE, message: error.message },
5217
5943
  ...(result.stream_events?.length ? { stream_events: result.stream_events } : {}),
@@ -5219,7 +5945,7 @@ export class SelfHostedNodeRuntime {
5219
5945
  timing: result.timing
5220
5946
  };
5221
5947
  }
5222
- const postedResult = job.openai_request.stream === true
5948
+ const postedResult = chatStreaming
5223
5949
  ? streamEventForwardingFailed
5224
5950
  ? {
5225
5951
  ...result,