0.1.0:3 - Show Public Key layout + /api/endpoints service-discovery

- showPublicKey now uses result.group: install command and raw key are each their own one-click copy box; description is brief
- /api/endpoints returns stable shape { vllm, parakeet, magpie } with base_url + model + ready, for other LAN services to consume without hardcoding Spark IPs
- health.py: parakeet/magpie now also expose base_url
- README: documented /api/endpoints shape
This commit is contained in:
Grant
2026-05-12 10:52:57 -05:00
parent 51804b2e5e
commit 2ba3da55b1
5 changed files with 121 additions and 36 deletions
+15 -1
View File
@@ -70,8 +70,22 @@ To sideload onto your Start9: `make install` (needs `host:` set in `~/.startos/c
- `known-issues.md` — known quirks and workarounds - `known-issues.md` — known quirks and workarounds
- `LICENSE` — MIT - `LICENSE` — MIT
## Service discovery API
Other services on your LAN can hit `GET /api/endpoints` to learn where the current model lives without hardcoding Spark IPs. Stable JSON shape:
```json
{
"vllm": { "ready": true, "base_url": "http://<spark-1-ip>:8888/v1", "model": "RedHatAI/Qwen3.6-35B-A3B-NVFP4", "openai_compat": true },
"parakeet":{ "ready": true, "base_url": "http://<spark-2-ip>:8000", "kind": "stt", "model": "nvidia/parakeet-tdt-0.6b-v3" },
"magpie": { "ready": false, "base_url": "http://<spark-2-ip>:9000", "kind": "tts" }
}
```
`base_url` is filled in whenever Configure Sparks has been completed (even if the underlying service isn't currently up). Pair the URL with `ready: true` to safely route traffic.
## Status ## Status
**v0.1** — local-only, single-cluster, no auth (trusts LAN). Five LLMs in the catalog: qwen3-vl (cluster), gemma4, qwen36, plus two legacy entries. Magpie surfaces red until its container is fixed. **v0.1** — local-only, single-cluster, no auth (trusts LAN). Five LLMs in the catalog: qwen3-vl (cluster), gemma4, qwen36, plus two legacy entries. Magpie surfaces red until its container is fixed.
v0.2 backlog (in `runbook.md` / commits): Parakeet/Magpie lifecycle controls, configurable flag tiers in UI, Open WebUI integration, magpie-tts fix. v0.2 in progress: service-discovery API, magpie crash fix, Parakeet/Magpie lifecycle, model download driving, spark-vllm-docker update checks, configurable flag tiers.
+20 -6
View File
@@ -30,24 +30,38 @@ async def check_vllm(settings: Settings) -> dict:
async def check_parakeet(settings: Settings) -> dict: async def check_parakeet(settings: Settings) -> dict:
base_url = (
f"http://{settings.spark2_host}:{settings.parakeet_port}"
if settings.spark2_host
else None
)
if not settings.spark2_host: if not settings.spark2_host:
return {"ok": False, "error": "spark2 not configured"} return {"ok": False, "error": "spark2 not configured", "base_url": base_url}
try: try:
async with httpx.AsyncClient(timeout=_TIMEOUT) as c: async with httpx.AsyncClient(timeout=_TIMEOUT) as c:
r = await c.get(f"http://{settings.spark2_host}:{settings.parakeet_port}/health") r = await c.get(f"http://{settings.spark2_host}:{settings.parakeet_port}/health")
r.raise_for_status() r.raise_for_status()
return {"ok": True, "detail": r.json()} return {"ok": True, "detail": r.json(), "base_url": base_url}
except Exception as e: except Exception as e:
return {"ok": False, "error": str(e)} return {"ok": False, "error": str(e), "base_url": base_url}
async def check_magpie(settings: Settings) -> dict: async def check_magpie(settings: Settings) -> dict:
base_url = (
f"http://{settings.spark2_host}:{settings.magpie_port}"
if settings.spark2_host
else None
)
if not settings.spark2_host: if not settings.spark2_host:
return {"ok": False, "error": "spark2 not configured"} return {"ok": False, "error": "spark2 not configured", "base_url": base_url}
try: try:
async with httpx.AsyncClient(timeout=_TIMEOUT) as c: async with httpx.AsyncClient(timeout=_TIMEOUT) as c:
r = await c.get(f"http://{settings.spark2_host}:{settings.magpie_port}/v1/health/ready") r = await c.get(f"http://{settings.spark2_host}:{settings.magpie_port}/v1/health/ready")
r.raise_for_status() r.raise_for_status()
return {"ok": True, "detail": r.json() if r.headers.get("content-type", "").startswith("application/json") else r.text} return {
"ok": True,
"detail": r.json() if r.headers.get("content-type", "").startswith("application/json") else r.text,
"base_url": base_url,
}
except Exception as e: except Exception as e:
return {"ok": False, "error": str(e)} return {"ok": False, "error": str(e), "base_url": base_url}
+31
View File
@@ -48,6 +48,37 @@ async def get_models() -> dict:
} }
@app.get("/api/endpoints")
async def get_endpoints() -> dict:
"""Service-discovery summary. Stable shape; other apps on the LAN can poll this
to learn the OpenAI-compatible vLLM endpoint, the Parakeet STT endpoint, and the
Magpie TTS endpoint without needing to know the individual Spark IPs."""
vllm, parakeet, magpie = await asyncio.gather(
check_vllm(settings),
check_parakeet(settings),
check_magpie(settings),
)
return {
"vllm": {
"ready": bool(vllm.get("ok")),
"base_url": vllm.get("base_url"),
"model": vllm.get("current_model"),
"openai_compat": True,
},
"parakeet": {
"ready": bool(parakeet.get("ok")),
"base_url": parakeet.get("base_url"),
"kind": "stt",
"model": (parakeet.get("detail") or {}).get("model") if isinstance(parakeet.get("detail"), dict) else None,
},
"magpie": {
"ready": bool(magpie.get("ok")),
"base_url": magpie.get("base_url"),
"kind": "tts",
},
}
@app.get("/api/status") @app.get("/api/status")
async def get_status() -> dict: async def get_status() -> dict:
vllm, parakeet, magpie = await asyncio.gather( vllm, parakeet, magpie = await asyncio.gather(
+49 -23
View File
@@ -44,40 +44,66 @@ export const showPublicKey = sdk.Action.withoutInput(
hosts.push({ user: cfg.spark2_user, host: cfg.spark2_host }) hosts.push({ user: cfg.spark2_user, host: cfg.spark2_host })
} }
let message: string if (hosts.length === 0) {
if (hosts.length > 0) { // Not yet configured — show just the raw key with instructions to come back.
const sshLines = hosts
.map(
(h) =>
`ssh ${h.user}@${h.host} "mkdir -p ~/.ssh && echo '$KEY' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"`,
)
.join('\n')
const oneLiner = `KEY='${key}'\n${sshLines}`
message =
'Paste this block into any machine that already has SSH access to your Sparks (your laptop or one of the Sparks itself). It grants this Spark Control service permission to log in.\n\n' +
oneLiner +
'\n\nYou will be prompted for the SSH password of each Spark once. After both commands succeed, run "Test Connection" or open the Web Interface to verify.'
} else {
message =
'Run the "Configure Sparks" action first, then come back to this one — once your Spark hostnames are saved, this message will include a ready-to-paste install command.\n\nFor reference, the raw public key is:\n\n' +
key
}
return { return {
version: '1' as const, version: '1' as const,
title: 'SSH Public Key', title: 'SSH Public Key',
message, message:
'Run the "Configure Sparks" action first to enter your Spark hostnames. Once that\'s saved, re-run this action and it will produce a ready-to-paste install command for granting access.',
result: { result: {
type: 'single' as const, type: 'single' as const,
name: 'Raw Public Key', name: 'Public Key',
description: description:
'The bare public key line. Copy the full message above for the ready-to-paste install command.', 'The bare ed25519 public key line. You can also wait until Configure Sparks is filled in to get a complete install command.',
value: key, value: key,
masked: false, masked: false,
copyable: true, copyable: true,
qr: false, qr: false,
}, },
} }
}
const sshLines = hosts
.map(
(h) =>
`ssh ${h.user}@${h.host} "mkdir -p ~/.ssh && echo '$KEY' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"`,
)
.join('\n')
const oneLiner = `KEY='${key}'\n${sshLines}`
return {
version: '1' as const,
title: 'SSH Public Key',
message:
'Run the command below on any machine that already has SSH access to your Sparks (typically your laptop). You will be prompted for the SSH password of each Spark once. After it completes, open the Web Interface to verify.',
result: {
type: 'group' as const,
name: 'Install command and raw key',
description: null,
value: [
{
type: 'single' as const,
name: 'Install command',
description:
'Copy this entire block and paste it into a terminal on your laptop. It will append the key to ~/.ssh/authorized_keys on each Spark.',
value: oneLiner,
masked: false,
copyable: true,
qr: false,
},
{
type: 'single' as const,
name: 'Raw public key',
description:
'Just the public key on its own, in case you prefer to install it manually.',
value: key,
masked: false,
copyable: true,
qr: false,
},
],
},
}
}, },
) )
+2 -2
View File
@@ -1,10 +1,10 @@
import { VersionInfo, IMPOSSIBLE } from '@start9labs/start-sdk' import { VersionInfo, IMPOSSIBLE } from '@start9labs/start-sdk'
export const v0_1_0 = VersionInfo.of({ export const v0_1_0 = VersionInfo.of({
version: '0.1.0:2', version: '0.1.0:3',
releaseNotes: { releaseNotes: {
en_US: en_US:
'Fully generic SSH user fields (no suggested default); generic host placeholders in Configure Sparks; Show Public Key emits a ready-to-paste install command using your configured hostnames; dashboard shows the OpenAI-compatible base URL + current model ID with one-click copy and a curl example.', 'Show Public Key: install command moved to its own copy box (cleaner than mixing it into the description). New /api/endpoints route for service discovery — other services on your LAN can GET it to learn vLLM/Parakeet/Magpie base URLs and current model without hardcoding Spark IPs.',
}, },
migrations: { migrations: {
up: async ({ effects }) => {}, up: async ({ effects }) => {},