motif64

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>