@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/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) ?? defaultLocalRunnerKindForAdapter(adapter),
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: isVisionModel(name, family),
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: capabilities.some((capability) => capability.toLowerCase().includes("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: deps?.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: deps?.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: deps?.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
- ? { usage: result.usage || openAiUsage(result.openai_response) }
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 fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
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: job.openai_request.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: job.openai_request.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 ?? null,
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 (job.openai_request.stream !== true || streamEventForwardingFailed) {
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 = job.openai_request.stream === true
5850
+ const postedResult = chatStreaming
5223
5851
  ? streamEventForwardingFailed
5224
5852
  ? {
5225
5853
  ...result,