@mcoda/mswarm 0.1.132 → 0.1.134

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
@@ -202,6 +202,7 @@ function resolvePersistentNodePath(nodePath) {
202
202
  const DEFAULT_EXPOSE_ALL_MODELS = true;
203
203
  const LOCAL_OPENAI_COMPATIBLE_ADAPTERS = new Set([
204
204
  "openai-compatible-local",
205
+ "strata-local",
205
206
  "vllm-local",
206
207
  "llama-cpp-local",
207
208
  "llamacpp-local"
@@ -244,6 +245,7 @@ const DEFAULT_VIDEO_GENERATIVE_OUTPUT_BYTES = 256 * 1024 * 1024;
244
245
  const GENERATIVE_RESPONSE_JSON_OVERHEAD_BYTES = 64 * 1024;
245
246
  const LOCAL_RUNNER_KIND_ALIASES = {
246
247
  vllm: "vllm",
248
+ strata: "strata",
247
249
  "llama-cpp": "llama-cpp",
248
250
  "llama.cpp": "llama-cpp",
249
251
  llamacpp: "llama-cpp",
@@ -526,6 +528,8 @@ function defaultLocalRunnerKindForAdapter(adapter) {
526
528
  const lookupKey = normalizeLookupKey(normalized);
527
529
  if (lookupKey === "vllm-local")
528
530
  return "vllm";
531
+ if (lookupKey === "strata-local")
532
+ return "strata";
529
533
  if (lookupKey === "llama-cpp-local" || lookupKey === "llamacpp-local")
530
534
  return "llama-cpp";
531
535
  return undefined;
@@ -2082,6 +2086,9 @@ export function mapMcodaAgentToSelfHostedModel(agent, config) {
2082
2086
  : inferredInputModalities);
2083
2087
  const outputModalities = localRunner?.outputModalities ?? inferGenerativeModalities(operations, "output");
2084
2088
  const publicModelId = localRunner?.publicModelId;
2089
+ const legacyConfig = isRecord(agent.config) ? agent.config : {};
2090
+ const legacyNativeBase = normalizeLocalRunnerKind(legacyConfig.runnerKind ?? legacyConfig.runner_kind) === "llama-cpp"
2091
+ ? mcodaAgentLocalRunnerBaseUrl(agent) : undefined;
2085
2092
  return {
2086
2093
  name: slug,
2087
2094
  provider: "mcoda",
@@ -2092,9 +2099,9 @@ export function mapMcodaAgentToSelfHostedModel(agent, config) {
2092
2099
  model_id: publicModelId || model,
2093
2100
  public_model_id: publicModelId,
2094
2101
  upstream_model: model,
2095
- base_url: localRunner?.baseUrl,
2096
- runner_kind: localRunner?.generativeRunnerKind ?? localRunner?.runnerKind,
2097
- auth_mode: localRunner?.authMode,
2102
+ base_url: localRunner?.baseUrl ?? legacyNativeBase ?? undefined,
2103
+ runner_kind: localRunner?.generativeRunnerKind ?? localRunner?.runnerKind ?? (legacyNativeBase ? "llama-cpp" : undefined),
2104
+ auth_mode: localRunner?.authMode ?? (legacyNativeBase ? normalizeLocalRunnerAuthMode(legacyConfig.authMode) : undefined),
2098
2105
  response_format_strategy: localRunner?.responseFormatStrategy,
2099
2106
  health_path: localRunner?.healthPath,
2100
2107
  models_path: localRunner?.modelsPath,
@@ -4395,6 +4402,13 @@ export class MswarmSelfHostedNodeClient {
4395
4402
  body: JSON.stringify(payload)
4396
4403
  }, this.timeoutMs);
4397
4404
  }
4405
+ async reportLoad(runtimeToken, payload) {
4406
+ return fetchJson(this.fetchImpl, `${this.gatewayBaseUrl}/v1/swarm/self-hosted/node/load`, {
4407
+ method: "POST",
4408
+ headers: { "content-type": "application/json", authorization: `Bearer ${runtimeToken}` },
4409
+ body: JSON.stringify(payload)
4410
+ }, Math.min(this.timeoutMs, 5000));
4411
+ }
4398
4412
  async uninstall(runtimeToken, payload) {
4399
4413
  return fetchJson(this.fetchImpl, `${this.gatewayBaseUrl}/v1/swarm/self-hosted/node/uninstall`, {
4400
4414
  method: "POST",
@@ -4459,6 +4473,15 @@ export class MswarmSelfHostedNodeClient {
4459
4473
  export class SelfHostedNodeRuntime {
4460
4474
  constructor(config, deps) {
4461
4475
  this.activeChatJobs = 0;
4476
+ this.activeRequestIds = new Set();
4477
+ this.lastModels = [];
4478
+ this.loadRuntimeToken = null;
4479
+ this.loadReportQueue = Promise.resolve();
4480
+ this.fastLoadUnsupported = false;
4481
+ this.nativeActiveJobs = 0;
4482
+ this.nativeProbeKeys = new Map();
4483
+ this.unavailableModelIds = [];
4484
+ this.nativeModelCapacity = {};
4462
4485
  this.activeAgenticJobs = 0;
4463
4486
  this.activeGenericJobs = 0;
4464
4487
  this.queuedLlmJobs = 0;
@@ -4665,7 +4688,7 @@ export class SelfHostedNodeRuntime {
4665
4688
  const maxConcurrency = Math.max(1, Math.floor(this.config.maxConcurrentJobs || 1), llmMaxConcurrency, this.config.genericJobsEnabled ? genericMaxConcurrency : 1);
4666
4689
  const activeChatJobs = nonNegativeTelemetryInteger(this.activeChatJobs);
4667
4690
  const activeAgenticJobs = nonNegativeTelemetryInteger(this.activeAgenticJobs);
4668
- const activeLlmJobs = activeChatJobs + activeAgenticJobs;
4691
+ const activeLlmJobs = Math.max(activeChatJobs + activeAgenticJobs, this.nativeActiveJobs);
4669
4692
  const activeGenericJobs = nonNegativeTelemetryInteger(this.activeGenericJobs);
4670
4693
  const queuedLlmJobs = nonNegativeTelemetryInteger(this.queuedLlmJobs);
4671
4694
  const queuedGenericJobs = nonNegativeTelemetryInteger(this.queuedGenericJobs);
@@ -5660,6 +5683,18 @@ export class SelfHostedNodeRuntime {
5660
5683
  }
5661
5684
  }
5662
5685
  async executeJob(job, options = {}) {
5686
+ this.activeRequestIds.add(job.request_id);
5687
+ try {
5688
+ return await this.executeTrackedJob(job, options);
5689
+ }
5690
+ finally {
5691
+ this.activeRequestIds.delete(job.request_id);
5692
+ // A product often starts its next stage immediately after this result.
5693
+ // Publish the newly free native slot before the gateway delivers it.
5694
+ await this.reportLoad().catch(() => undefined);
5695
+ }
5696
+ }
5697
+ async executeTrackedJob(job, options) {
5663
5698
  const startedAt = Date.now();
5664
5699
  // Resolved once, before any work: the slot is taken from the moment the job
5665
5700
  // is accepted, and a class settled later would leave the count attributed to
@@ -6027,6 +6062,111 @@ export class SelfHostedNodeRuntime {
6027
6062
  return result;
6028
6063
  }
6029
6064
  }
6065
+ async refreshNativeLoad() {
6066
+ const runners = new Map();
6067
+ for (const model of this.lastModels) {
6068
+ const isStrata = model.runner_kind === "strata" || model.adapter === "strata-local";
6069
+ if (model.exposed === false || !model.base_url ||
6070
+ (!isStrata && model.runner_kind !== "llama-cpp" && model.adapter !== "llama-cpp-local"))
6071
+ continue;
6072
+ try {
6073
+ const url = new URL(model.base_url);
6074
+ if (!["127.0.0.1", "localhost", "[::1]"].includes(url.hostname))
6075
+ continue;
6076
+ const endpoint = new URL(isStrata ? "/v1/status" : "/slots", url).toString();
6077
+ const models = runners.get(endpoint) || [];
6078
+ models.push(model);
6079
+ runners.set(endpoint, models);
6080
+ }
6081
+ catch { /* A malformed endpoint is handled by normal discovery health. */ }
6082
+ }
6083
+ let active = 0;
6084
+ const modelCapacity = {};
6085
+ const unavailable = [];
6086
+ await Promise.all([...runners].map(async ([endpoint, models]) => {
6087
+ try {
6088
+ const authenticated = models.find((model) => model.auth_mode === "bearer" && model.source_agent_id);
6089
+ let apiKey;
6090
+ if (authenticated?.source_agent_id) {
6091
+ let pending = this.nativeProbeKeys.get(authenticated.source_agent_id);
6092
+ if (!pending) {
6093
+ pending = this.mcodaAgentAuthResolver({ id: authenticated.source_agent_id,
6094
+ slug: authenticated.source_agent_slug || authenticated.name }).catch(() => undefined);
6095
+ this.nativeProbeKeys.set(authenticated.source_agent_id, pending);
6096
+ }
6097
+ apiKey = await pending;
6098
+ }
6099
+ const response = await this.fetchImpl(endpoint, { signal: AbortSignal.timeout(1500),
6100
+ ...(apiKey ? { headers: { authorization: `Bearer ${apiKey}` } } : {}) });
6101
+ const isStrata = new URL(endpoint).pathname === "/v1/status";
6102
+ // llama.cpp slot introspection is optional. Strata status also proves the model is loaded.
6103
+ if (!isStrata && [401, 403, 404].includes(response.status))
6104
+ return;
6105
+ if (!response.ok)
6106
+ throw new Error("runner_load_unavailable");
6107
+ const slots = await response.json();
6108
+ if (isStrata) {
6109
+ const concurrency = isRecord(slots) && isRecord(slots.concurrency) ? slots.concurrency.serving : undefined;
6110
+ const inFlight = isRecord(slots) && isRecord(slots.activity) ? slots.activity.in_flight : undefined;
6111
+ if (!isRecord(slots) || slots.loaded !== true ||
6112
+ typeof concurrency !== "number" || !Number.isSafeInteger(concurrency) || concurrency < 1 ||
6113
+ typeof inFlight !== "number" || !Number.isSafeInteger(inFlight) || inFlight < 0) {
6114
+ throw new Error("runner_load_unavailable");
6115
+ }
6116
+ const processing = Math.min(concurrency, inFlight);
6117
+ active += processing;
6118
+ const capacity = { max_concurrency: concurrency, active_jobs: processing,
6119
+ queued_jobs: Math.max(0, inFlight - concurrency), free_slots: Math.max(0, concurrency - inFlight),
6120
+ pool: `runner:${createHash("sha256").update(endpoint).digest("hex").slice(0, 16)}` };
6121
+ for (const model of models)
6122
+ modelCapacity[model.model_id || model.model || model.name] = capacity;
6123
+ return;
6124
+ }
6125
+ if (!Array.isArray(slots))
6126
+ return;
6127
+ const processing = slots.filter((slot) => isRecord(slot) && slot.is_processing === true).length;
6128
+ active += processing;
6129
+ if (slots.length > 0) {
6130
+ const capacity = { max_concurrency: slots.length, active_jobs: processing, queued_jobs: 0,
6131
+ free_slots: slots.length - processing, pool: `runner:${createHash("sha256").update(endpoint).digest("hex").slice(0, 16)}` };
6132
+ for (const model of models)
6133
+ modelCapacity[model.model_id || model.model || model.name] = capacity;
6134
+ }
6135
+ }
6136
+ catch {
6137
+ unavailable.push(...models.map((model) => model.model_id || model.model || model.name));
6138
+ }
6139
+ }));
6140
+ this.nativeActiveJobs = active;
6141
+ this.nativeModelCapacity = modelCapacity;
6142
+ this.unavailableModelIds = [...new Set(unavailable)];
6143
+ }
6144
+ reportLoad() {
6145
+ // Serialize probes and writes so an older busy snapshot cannot overwrite
6146
+ // the completion snapshot when the periodic reporter is already running.
6147
+ const pending = this.loadReportQueue.then(() => this.reportLoadSnapshot());
6148
+ this.loadReportQueue = pending.catch(() => undefined);
6149
+ return pending;
6150
+ }
6151
+ async reportLoadSnapshot() {
6152
+ if (this.config.loadReportingEnabled === false || this.fastLoadUnsupported || !this.loadRuntimeToken)
6153
+ return;
6154
+ await this.refreshNativeLoad();
6155
+ const telemetry = this.buildLoadTelemetry({ models: this.lastModels });
6156
+ try {
6157
+ await this.gateway.reportLoad(this.loadRuntimeToken, {
6158
+ node_id: this.config.nodeId,
6159
+ capacity: { ...telemetry, active_request_ids: [...this.activeRequestIds],
6160
+ unavailable_model_ids: this.unavailableModelIds, model_capacity: this.nativeModelCapacity }
6161
+ });
6162
+ }
6163
+ catch (error) {
6164
+ if (isHttpStatusError(error, 404))
6165
+ this.fastLoadUnsupported = true;
6166
+ else
6167
+ throw error;
6168
+ }
6169
+ }
6030
6170
  async runOnce() {
6031
6171
  const enrollment = await this.ensureEnrolled();
6032
6172
  let status = "online";
@@ -6057,6 +6197,10 @@ export class SelfHostedNodeRuntime {
6057
6197
  this.lifecyclePollingDisabled = true;
6058
6198
  }
6059
6199
  const discoveryLatencyMs = Date.now() - startedAt;
6200
+ if (models.length || status === "online")
6201
+ this.lastModels = models;
6202
+ this.nativeProbeKeys.clear();
6203
+ await this.refreshNativeLoad();
6060
6204
  const capabilityPayload = await this.buildCapabilityHeartbeatPayload(enrollment.runtimeToken);
6061
6205
  const loadTelemetry = this.buildLoadTelemetry({
6062
6206
  models,
@@ -6077,6 +6221,9 @@ export class SelfHostedNodeRuntime {
6077
6221
  max_concurrent_llm_jobs: loadTelemetry.max_concurrent_llm_jobs,
6078
6222
  max_concurrent_generic_jobs: loadTelemetry.max_concurrent_generic_jobs,
6079
6223
  active_jobs: loadTelemetry.active_jobs,
6224
+ active_request_ids: [...this.activeRequestIds],
6225
+ unavailable_model_ids: this.unavailableModelIds,
6226
+ model_capacity: this.nativeModelCapacity,
6080
6227
  queued_jobs: loadTelemetry.queued_jobs,
6081
6228
  free_slots: loadTelemetry.free_slots,
6082
6229
  drain_mode: loadTelemetry.drain_mode,
@@ -6148,6 +6295,7 @@ export class SelfHostedNodeRuntime {
6148
6295
  ...(loadTelemetry.hardware_pressure ? { hardware_pressure: loadTelemetry.hardware_pressure } : {})
6149
6296
  };
6150
6297
  const heartbeatResponse = await this.gateway.heartbeat(enrollment.runtimeToken, heartbeatPayload);
6298
+ this.loadRuntimeToken = enrollment.runtimeToken;
6151
6299
  return {
6152
6300
  enrolled: enrollment.enrolled,
6153
6301
  status,
@@ -6402,6 +6550,8 @@ export class SelfHostedNodeRuntime {
6402
6550
  startDaemon() {
6403
6551
  let stopped = false;
6404
6552
  let timer = null;
6553
+ let loadTimer = null;
6554
+ let loadReporting = false;
6405
6555
  let pollTimer = null;
6406
6556
  let pollTimerDueAt = 0;
6407
6557
  let polling = false;
@@ -6425,6 +6575,10 @@ export class SelfHostedNodeRuntime {
6425
6575
  };
6426
6576
  const halt = () => {
6427
6577
  stopped = true;
6578
+ if (loadTimer) {
6579
+ clearInterval(loadTimer);
6580
+ loadTimer = null;
6581
+ }
6428
6582
  if (timer) {
6429
6583
  clearTimeout(timer);
6430
6584
  timer = null;
@@ -6574,6 +6728,13 @@ export class SelfHostedNodeRuntime {
6574
6728
  .finally(schedule);
6575
6729
  }, revoked ? REVOKED_RECHECK_INTERVAL_MS : this.config.heartbeatIntervalSeconds * 1000);
6576
6730
  };
6731
+ loadTimer = setInterval(() => {
6732
+ if (stopped || revoked || loadReporting)
6733
+ return;
6734
+ loadReporting = true;
6735
+ void this.reportLoad().catch((error) => { noteRevoked(error); }).finally(() => { loadReporting = false; });
6736
+ }, 2000);
6737
+ loadTimer.unref?.();
6577
6738
  void this.runOnce()
6578
6739
  .then(noteAlive)
6579
6740
  .catch((error) => {