Bump to 0.1.0:1 — portability + endpoint display
- configureSparks.ts: generic placeholders (e.g. 192.168.1.10), no Alice-specific IPs; descriptions explain the role of each node instead of naming his hardware - showPublicKey.ts: reads sparkConfig.yaml; emits a ready-to-paste one-liner (KEY='...' followed by 'ssh user@host "echo $KEY >> authorized_keys"' for each configured Spark). Falls back to generic instructions if Configure Sparks hasn't been run yet. - /api/status now includes vllm.base_url for the OpenAI endpoint - New endpoint panel in UI: base URL + model ID rows with copy buttons + collapsible curl example - Bump version to 0.1.0:1
This commit is contained in:
+13
-3
@@ -7,16 +7,26 @@ _TIMEOUT = 3.0
|
||||
|
||||
|
||||
async def check_vllm(settings: Settings) -> dict:
|
||||
base_url = (
|
||||
f"http://{settings.spark1_host}:{settings.vllm_port}/v1"
|
||||
if settings.spark1_host
|
||||
else None
|
||||
)
|
||||
if not settings.spark1_host:
|
||||
return {"ok": False, "error": "spark1 not configured"}
|
||||
return {"ok": False, "error": "spark1 not configured", "base_url": base_url}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=_TIMEOUT) as c:
|
||||
r = await c.get(f"http://{settings.spark1_host}:{settings.vllm_port}/v1/models")
|
||||
r.raise_for_status()
|
||||
ids = [m["id"] for m in r.json().get("data", [])]
|
||||
return {"ok": True, "current_model": ids[0] if ids else None, "all": ids}
|
||||
return {
|
||||
"ok": True,
|
||||
"current_model": ids[0] if ids else None,
|
||||
"all": ids,
|
||||
"base_url": base_url,
|
||||
}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
return {"ok": False, "error": str(e), "base_url": base_url}
|
||||
|
||||
|
||||
async def check_parakeet(settings: Settings) -> dict:
|
||||
|
||||
@@ -83,6 +83,52 @@ function renderCurrent(status) {
|
||||
c.innerHTML = `<strong>${label}</strong>`;
|
||||
}
|
||||
|
||||
function renderEndpoint(status) {
|
||||
const v = status.vllm || {};
|
||||
const panel = el('#endpoint-panel');
|
||||
const ready = v.ok && v.current_model && v.base_url;
|
||||
panel.classList.toggle('hidden', !ready);
|
||||
if (!ready) return;
|
||||
el('#ep-url').textContent = v.base_url;
|
||||
el('#ep-model').textContent = v.current_model;
|
||||
const snippet =
|
||||
`curl -s ${v.base_url}/chat/completions \\
|
||||
-H 'content-type: application/json' \\
|
||||
-d '{
|
||||
"model": "${v.current_model}",
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
}'`;
|
||||
el('#ep-curl-snippet').textContent = snippet;
|
||||
}
|
||||
|
||||
function setupCopyButtons() {
|
||||
document.body.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.copy-btn');
|
||||
if (!btn) return;
|
||||
const targetSel = btn.dataset.copy;
|
||||
if (!targetSel) return;
|
||||
const target = el(targetSel);
|
||||
if (!target) return;
|
||||
const text = target.textContent;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
const original = btn.textContent;
|
||||
btn.classList.add('copied');
|
||||
btn.textContent = 'Copied';
|
||||
setTimeout(() => {
|
||||
btn.classList.remove('copied');
|
||||
btn.textContent = original;
|
||||
}, 1400);
|
||||
} catch {
|
||||
// Clipboard API may fail over plain HTTP; fall back to selection
|
||||
const range = document.createRange();
|
||||
range.selectNode(target);
|
||||
window.getSelection().removeAllRanges();
|
||||
window.getSelection().addRange(range);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderHealth(status) {
|
||||
function setDot(id, ok, payload) {
|
||||
const item = el(id);
|
||||
@@ -221,6 +267,7 @@ async function pollStatus() {
|
||||
state.configured = status.configured;
|
||||
renderBanner(status);
|
||||
renderCurrent(status);
|
||||
renderEndpoint(status);
|
||||
renderHealth(status);
|
||||
if (status.current_swap_job && status.current_swap_job !== state.swap_job_id) {
|
||||
attachToSwap(status.current_swap_job, /*needsBackfill=*/true);
|
||||
@@ -342,6 +389,7 @@ function appendLog(line) {
|
||||
}
|
||||
|
||||
async function init() {
|
||||
setupCopyButtons();
|
||||
await loadModels();
|
||||
await pollStatus();
|
||||
setInterval(pollStatus, 5000);
|
||||
|
||||
@@ -24,6 +24,25 @@
|
||||
<span>Run the <em>Configure Sparks</em> action in StartOS to set hostnames, then run <em>Test Connection</em>.</span>
|
||||
</section>
|
||||
|
||||
<section id="endpoint-panel" class="endpoint-panel hidden">
|
||||
<div class="ep-title muted small">OpenAI-compatible endpoint</div>
|
||||
<div class="ep-row">
|
||||
<span class="ep-label">Base URL</span>
|
||||
<code class="ep-value" id="ep-url">—</code>
|
||||
<button class="copy-btn" data-copy="#ep-url" title="Copy base URL">Copy</button>
|
||||
</div>
|
||||
<div class="ep-row">
|
||||
<span class="ep-label">Model ID</span>
|
||||
<code class="ep-value" id="ep-model">—</code>
|
||||
<button class="copy-btn" data-copy="#ep-model" title="Copy model ID">Copy</button>
|
||||
</div>
|
||||
<details class="ep-curl">
|
||||
<summary class="muted small">curl example</summary>
|
||||
<pre id="ep-curl-snippet" class="snippet"></pre>
|
||||
<button class="copy-btn small" data-copy="#ep-curl-snippet">Copy snippet</button>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section id="swap-panel" class="swap-panel hidden">
|
||||
<div class="swap-header">
|
||||
<span class="spinner"></span>
|
||||
|
||||
@@ -63,6 +63,77 @@ main {
|
||||
}
|
||||
.banner em { font-style: normal; background: rgba(245, 158, 11, 0.15); padding: 2px 6px; border-radius: 4px; }
|
||||
|
||||
/* ===== Endpoint panel ===== */
|
||||
|
||||
.endpoint-panel {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.ep-title { margin-bottom: 8px; letter-spacing: 0.05em; text-transform: uppercase; }
|
||||
.ep-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 0;
|
||||
}
|
||||
.ep-row + .ep-row { border-top: 1px solid var(--border); }
|
||||
.ep-label {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
min-width: 78px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ep-value {
|
||||
flex: 1;
|
||||
font: 13px/1.4 ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
||||
background: var(--surface-2);
|
||||
padding: 4px 8px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.copy-btn {
|
||||
appearance: none;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
padding: 4px 10px;
|
||||
border-radius: 5px;
|
||||
font: 12px/1 inherit;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.copy-btn:hover { color: var(--text); border-color: #34343c; }
|
||||
.copy-btn.copied {
|
||||
color: var(--accent);
|
||||
border-color: rgba(74, 222, 128, 0.4);
|
||||
background: rgba(74, 222, 128, 0.08);
|
||||
}
|
||||
.copy-btn.small { padding: 3px 8px; font-size: 11px; }
|
||||
|
||||
.ep-curl { margin-top: 8px; }
|
||||
.ep-curl summary { cursor: pointer; padding: 4px 0; }
|
||||
.ep-curl[open] summary { margin-bottom: 6px; }
|
||||
.snippet {
|
||||
background: #08080b;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
margin: 0 0 8px;
|
||||
font: 12px/1.55 ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
||||
color: #c7c7d1;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* ===== Swap panel ===== */
|
||||
|
||||
.swap-panel {
|
||||
|
||||
Reference in New Issue
Block a user