From 8579517a35ef1865fc9b428899d73d52dcb27a14 Mon Sep 17 00:00:00 2001 From: historia Date: Wed, 2 Sep 2026 01:26:09 -0400 Subject: feat: sglang backend support --- app/backends/probe.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) (limited to 'app/backends/probe.py') diff --git a/app/backends/probe.py b/app/backends/probe.py index 818aadb..d54a9f3 100644 --- a/app/backends/probe.py +++ b/app/backends/probe.py @@ -26,7 +26,7 @@ it stays cheap to import alongside ``backends.common``. import json import urllib.parse import urllib.request -from typing import Optional +from typing import List, Optional from backends import common @@ -35,6 +35,7 @@ IDENTITY_FASTER = "faster" IDENTITY_QWEN_CUSTOM = "qwen-custom" IDENTITY_QWEN_CLONE = "qwen-clone" IDENTITY_QWEN_DESIGN = "qwen-design" +IDENTITY_SGLOMNI = "sglomni" # Endpoint names the converter resolves for each qwen demo server (see # converter.clients QwenTTSClient). Mirror them here so identification matches @@ -77,7 +78,7 @@ def _get_json(url: str, timeout: float) -> Optional[dict]: def _identify_health(base: str, timeout: float) -> Optional[str]: - """Identify audio.cpp / faster from their ``/health`` responses.""" + """Identify audio.cpp / faster / sglang-omni from their /health responses.""" payload = _get_json(f"{base}/health", timeout) if payload is None: return None @@ -93,6 +94,15 @@ def _identify_health(base: str, timeout: float) -> Optional[str]: if isinstance(entries, list) and entries \ and any(isinstance(e, dict) and e.get("id") for e in entries): return IDENTITY_AUDIOCPP + # sglang-omni's /health reports {"status": "healthy", "stages": [...]} + # (200 when serving, 503 with "unhealthy" while booting). A 503 body + # still parses as JSON here, so require the healthy word explicitly — + # an "unhealthy" sgl-omni must not count as usable. The pipeline + # "stages" list is confirmed as a secondary mark (present on every + # sgl-omni 0.1.x server) before trusting the generic-sounding status. + if payload.get("status") == "healthy" \ + and isinstance(payload.get("stages"), list): + return IDENTITY_SGLOMNI return None @@ -184,3 +194,48 @@ def faster_model_loaded(url: str, timeout: float = DEFAULT_TIMEOUT) -> bool: """ payload = health_payload(url, timeout) return bool(payload and payload.get("model_loaded")) + + +def sglomni_served_model(url: str, + timeout: float = DEFAULT_TIMEOUT) -> Optional[str]: + """The HuggingFace repo id a sglang-omni server at URL hosts, or None. + + ``GET /v1/models`` answers ``{"data": [{"id": }, ...]}`` + with exactly one entry (one model per server process) — the same + model-identity role qwen's probe plays for its three demos. Used to + name the running model in statuses and to decide when a managed + server must be restarted to host the model a run selected. + """ + if not url: + return None + models = _get_json(f"{url.rstrip('/')}/v1/models", timeout) + if models is None: + return None + entries = models.get("data") + if isinstance(entries, list) and entries \ + and isinstance(entries[0], dict): + model_id = entries[0].get("id") + if isinstance(model_id, str) and model_id: + return model_id + return None + + +def sglomni_voice_names(url: str, + timeout: float = DEFAULT_TIMEOUT) -> Optional[List[str]]: + """The uploaded voice names registered on a sglang-omni server, or None. + + ``GET /v1/audio/voices?names_only=true`` answers + ``{"uploaded_voice_names": [...]}`` — the server-side voices a remote + Convert form can offer in its voice picker (uploaded clips persist + across server restarts). None when the URL does not answer. + """ + if not url: + return None + payload = _get_json(f"{url.rstrip('/')}/v1/audio/voices?names_only=true", + timeout) + if payload is None: + return None + names = payload.get("uploaded_voice_names") + if isinstance(names, list): + return [str(name) for name in names if isinstance(name, str) and name] + return None -- cgit v1.2.3