m64_dfd55abca230 / human/index.html
173 lines, 9.1 KBraw
1<!doctype html>2<html lang="en">3<meta charset="utf-8">4<meta name="viewport" content="width=device-width, initial-scale=1">5<title>Qwen3.8-Flash-Next on Basalt · model endpoint</title>6<style>7 :root { color-scheme: light dark; --fg: #1d1d1f; --bg: #f6f5f2; --card: #fff; --muted: #6b6b70; --line: #d8d6d0; --accent: #0f766e; --code: #efede8; }8 @media (prefers-color-scheme: dark) { :root { --fg: #ecebe8; --bg: #161616; --card: #1f1f1f; --muted: #9a9a9f; --line: #333; --accent: #2dd4bf; --code: #262626; } }9 * { box-sizing: border-box; }10 body { margin: 0; padding: 24px 16px; font: 15px/1.45 system-ui, sans-serif; color: var(--fg); background: var(--bg); }11 main { max-width: 820px; margin: 0 auto; }12 h1 { font-size: 20px; margin: 0 0 4px; }13 h2 { font-size: 15px; margin: 22px 0 6px; }14 .sub { color: var(--muted); margin: 0 0 16px; font-size: 13px; }15 a { color: var(--accent); }16 code { background: var(--code); border-radius: 4px; padding: 0 4px; font-size: 13px; }17 pre { background: var(--code); border-radius: 6px; padding: 10px; overflow-x: auto; font-size: 12.5px; margin: 0; }18 .cmd { display: flex; gap: 8px; align-items: flex-start; margin: 4px 0 10px; }19 .cmd pre { flex: 1; min-width: 0; }20 .note { color: var(--muted); font-size: 13px; margin: 6px 0; }21 ul.facts { margin: 6px 0; padding-left: 20px; }22 ul.facts li { margin: 3px 0; }23 button, textarea { font: inherit; color: inherit; background: transparent; border: 1px solid var(--line); border-radius: 6px; padding: 6px 10px; }24 button { cursor: pointer; }25 button.primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }26 @media (prefers-color-scheme: dark) { button.primary { color: #0b1f1d; } }27 button:disabled { opacity: .5; cursor: progress; }28 .endpoint { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; font-size: 14px; overflow-wrap: anywhere; }29 form { display: grid; grid-template-columns: 1fr auto; gap: 8px; }30 textarea { resize: vertical; min-height: 44px; }31 #out { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; white-space: pre-wrap; overflow-wrap: anywhere; min-height: 3em; margin-top: 8px; }32 #out.err { color: #dc2626; }33 #state { color: var(--muted); font-size: 13px; min-height: 1.4em; margin-top: 4px; }34</style>35<main>36 <h1>Qwen3.8-Flash-Next on Basalt, as a model endpoint</h1>37 <p class="sub">OpenAI-compatible chat from a local RTX 5090 (Basalt engine, 384k context), for opencode running on your own machine. API: <a href="../agent">/agent</a> · <span id="phase">checking…</span></p>3839 <div class="endpoint">Base URL: <code id="base">…</code> · model <code>qwen3.8-flash-next-iq3_s-basalt-384k</code></div>4041 <h2>1. Connect opencode once, then just run it</h2>42 <p class="note">Needs opencode installed (<code>npm i -g opencode-ai</code>). Run once, on your machine:</p>43 <div class="cmd"><pre id="login">…</pre><button type="button" data-copy="login">copy</button></div>44 <p class="note">Then, in any folder you want opencode to work on:</p>45 <div class="cmd"><pre id="plain">opencode</pre><button type="button" data-copy="plain">copy</button></div>46 <p class="note">opencode reads this capsule's settings from <a href="../.well-known/opencode">/.well-known/opencode</a> on every start, so nothing is written to your config. Undo with <code>opencode auth logout</code>.</p>4748 <h2>2. Or without logging in</h2>49 <p class="note">One run only: the provider is passed inline and merged over your own config.</p>50 <div class="cmd"><pre id="run">…</pre><button type="button" data-copy="run">copy</button></div>51 <details><summary class="note">Or as a permanent config file</summary>52 <p class="note">Save as <code>~/.config/opencode/opencode.json</code> (every project) or <code>opencode.json</code> in a project's root.</p>53 <div class="cmd"><pre id="config">…</pre><button type="button" data-copy="config">copy</button></div>54 </details>5556 <h2>3. Or, where m64 is installed</h2>57 <div class="cmd"><pre id="oneliner">m64 qwen38-flash-next-basalt-5090 --ui opencode</pre><button type="button" data-copy="oneliner">copy</button></div>58 <p class="note">Serves this capsule on that machine (or reuses it) and starts opencode with a generated config pointing at it, in the current directory.</p>5960 <h2>What this means</h2>61 <ul class="facts">62 <li>opencode, your files, your shell and every tool it runs stay on your machine. Only the model calls (your prompts and the file contents opencode sends as context) come to this capsule.</li>63 <li>The model runs here, on the RTX 5090: nothing leaves this machine. The capsule's container has no network access (it needs none; m64 grants it only with <code>--allow-external-network</code>).</li>64 <li>One request at a time: a second opencode session waits for the first one's reply.</li>65 <li>There is no token on the tailnet: anyone on your tailnet can use this endpoint.</li>66 </ul>6768 <h2>Quick test</h2>69 <form id="f">70 <textarea id="text" rows="2" required>In one sentence, what is a tmux session?</textarea>71 <button id="go" class="primary" type="submit">Send</button>72 </form>73 <div id="out"></div>74 <div id="state"></div>75</main>76<script>77const $ = (id) => document.getElementById(id);78const capsuleUrl = location.origin + location.pathname.replace(/\/human\/.*$/, "");79const base = capsuleUrl + "/v1";80$("base").textContent = base;81$("login").textContent = "opencode auth login " + capsuleUrl;82const provider = {83 basalt: {84 npm: "@ai-sdk/openai-compatible",85 name: "Qwen3.8-Flash-Next on Basalt (capsule)",86 options: { baseURL: base },87 models: { "qwen3.8-flash-next-iq3_s-basalt-384k": { name: "Qwen3.8-Flash-Next (Basalt, 384k)", tool_call: true } },88 },89};90const model = "basalt/qwen3.8-flash-next-iq3_s-basalt-384k";91$("config").textContent = JSON.stringify({ "$schema": "https://opencode.ai/config.json", provider, model }, null, 2);92// Single-quoted for the shell: the JSON has no single quotes (the URL comes from location).93$("run").textContent = "OPENCODE_CONFIG_CONTENT='" + JSON.stringify({ provider, model }) + "' opencode";9495fetch("../status").then((r) => r.json()).then((s) => {96 $("phase").textContent = s.phase === "ready" ? "ready" : s.phase + (s.detail ? " (" + s.detail + ")" : "");97}).catch(() => { $("phase").textContent = "status unavailable"; });9899for (const b of document.querySelectorAll("[data-copy]")) {100 b.addEventListener("click", async () => {101 const text = $(b.dataset.copy).textContent;102 try { await navigator.clipboard.writeText(text); }103 catch (e) { // plain http is not a secure context: select and copy the old way104 const range = document.createRange();105 range.selectNodeContents($(b.dataset.copy));106 getSelection().removeAllRanges();107 getSelection().addRange(range);108 document.execCommand("copy");109 }110 b.textContent = "copied";111 setTimeout(() => { b.textContent = "copy"; }, 1500);112 });113}114115// Streams the reply with the same SSE route opencode uses.116$("f").addEventListener("submit", async (ev) => {117 ev.preventDefault();118 const text = $("text").value.trim();119 if (!text) return;120 $("go").disabled = true;121 $("out").textContent = "";122 $("out").className = "";123 const t0 = performance.now();124 let first = null;125 $("state").textContent = "waiting…";126 try {127 const r = await fetch("../v1/chat/completions", {128 method: "POST", headers: { "Content-Type": "application/json" },129 body: JSON.stringify({ messages: [{ role: "user", content: text }], stream: true, max_tokens: 800 }),130 });131 if (!r.ok || !r.body) {132 const d = await r.text();133 $("out").className = "err";134 $("out").textContent = "HTTP " + r.status + ": " + d;135 return;136 }137 const reader = r.body.getReader();138 const dec = new TextDecoder();139 let buf = "";140 for (;;) {141 const { value, done } = await reader.read();142 if (done) break;143 buf += dec.decode(value, { stream: true });144 let i;145 while ((i = buf.indexOf("\n")) >= 0) {146 const line = buf.slice(0, i).trim();147 buf = buf.slice(i + 1);148 if (!line.startsWith("data:")) continue;149 const data = line.slice(5).trim();150 if (data === "[DONE]") continue;151 try {152 const d = JSON.parse(data);153 if (d.error) { $("out").className = "err"; $("out").textContent += "\n" + JSON.stringify(d.error); continue; }154 const piece = d.choices && d.choices[0] && d.choices[0].delta && d.choices[0].delta.content;155 if (piece) {156 if (first === null) first = performance.now();157 $("out").textContent += piece;158 }159 } catch (e) { /* partial or non-JSON line */ }160 }161 $("state").textContent = "streaming… " + ((performance.now() - t0) / 1000).toFixed(1) + " s";162 }163 const total = ((performance.now() - t0) / 1000).toFixed(1);164 $("state").textContent = "done in " + total + " s" + (first ? " (first token " + ((first - t0) / 1000).toFixed(1) + " s)" : "");165 } catch (e) {166 $("out").className = "err";167 $("out").textContent = "Request failed: " + e;168 } finally {169 $("go").disabled = false;170 }171});172</script>173</html>