Add Spark Control hardware backend (diarize, queue, discovery)

This commit is contained in:
Keysat
2026-06-13 13:36:04 -05:00
parent 705807e286
commit 84d56c94c9
6 changed files with 1435 additions and 172 deletions
+92
View File
@@ -0,0 +1,92 @@
// Resolves which (URL, model) the relay should use for each operator-
// hardware pipeline. Spark Control is the SINGLE host the relay
// talks to — it owns transcribe (/v1/audio/transcriptions),
// diarize-chunk (/api/audio/diarize-chunk), AND analyze
// (/v1/chat/completions, OpenAI-compatible) on the same port. The
// discovery JSON Spark Control serves at /api/endpoints lists the
// backing services SC delegates to internally (e.g. a Parakeet
// wrapper, a vLLM instance) along with their ready state + currently-
// loaded model name; the relay reads it ONLY to learn (a) whether
// each service is ready, and (b) which model name to send in the
// upstream request body. The per-service base URLs in the discovery
// JSON are informational — clients hit Spark Control directly, not
// those internal URLs.
//
// Priority order, per pipeline:
//
// 1. Spark Control says ready → use the SC base URL + the model
// name discovery reports for that
// service.
//
// 2. SC says not_ready → return null URL + a blocked_reason
// string. Route handlers surface
// this to the user instead of trying
// a doomed call.
//
// 3. SC unreachable or no URL → null URL; planBackend treats the
// hardware path as not configured.
//
// History:
// - pre-v0.2.84: operator-typed override URLs (relay_parakeet_*,
// relay_gemma_*) won over discovery. Removed.
// - pre-v0.2.85: discovery's per-service base_urls were used
// directly, so transcribe went to a different host (the Parakeet
// wrapper) than diarize (Spark Control). That meant the relay
// was talking to two hosts for one logical operation, and the
// transcribe wrapper didn't have the diarize endpoint. Now: SC
// is the single host. The per-service base_urls in discovery
// are informational — used only for the operator's awareness in
// the dashboard's Service Discovery health line.
import { getSparkServiceState } from "./spark-control.js";
export async function resolveHardwareConfig(cfg) {
const sparkUrl = (cfg.relay_spark_control_url || "").trim();
// Spark Control base URL with the /api/endpoints suffix and any
// trailing slash stripped. Used as the URL for every operator-
// hardware call: transcribe, diarize-chunk, analyze.
const sparkBase = sparkUrl.replace(/\/$/, "").replace(/\/api\/endpoints$/, "");
const transcribe = await resolveOne({ sparkUrl, sparkBase, kind: "transcribe" });
const analyze = await resolveOne({ sparkUrl, sparkBase, kind: "analyze" });
const tts = await resolveOne({ sparkUrl, sparkBase, kind: "tts" });
return {
transcribe,
analyze,
tts,
sparkBase,
};
}
async function resolveOne({ sparkUrl, sparkBase, kind }) {
if (!sparkUrl || !sparkBase) {
return { url: null, model: null, source: null };
}
const state = await getSparkServiceState(sparkUrl, kind);
if (state.status === "ready") {
return {
// ALWAYS Spark Control's base URL — never the delegate's
// base_url. SC owns the wire-facing endpoint; the delegate
// URL is just where SC routes the request internally.
url: sparkBase,
// Model name comes from discovery so we send the right
// `model` field upstream. SC tells us what model is currently
// loaded on its parakeet / vllm delegate.
model: state.model,
source: "spark-control",
};
}
if (state.status === "not_ready") {
return {
url: null,
model: null,
source: "spark-control",
blocked_reason: state.reason,
};
}
// status === "unknown" → discovery unreachable or that service
// isn't listed. Return null URL; the route handler surfaces the
// error to the user.
return { url: null, model: null, source: null };
}