motif64

m64_04b2a07e4eac / human/index.html

172 lines, 9.0 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>DeepSeek V4.1 Flash · 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>DeepSeek V4.1 Flash, as a model endpoint</h1>37  <p class="sub">OpenAI-compatible chat via OpenRouter, 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>deepseek-v4.1-flash</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 deepseek-flash-openrouter-gb10 --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, and from here to OpenRouter.</li>63    <li>The OpenRouter key stays in the m64 gate: this capsule's container never holds it. It is served with <code>--allow-external-network</code> (it needs openrouter.ai): the public internet, never your LAN or tailnet.</li>64    <li>There is no token on the tailnet: anyone on your tailnet can use this endpoint, and so spend the key.</li>65  </ul>6667  <h2>Quick test</h2>68  <form id="f">69    <textarea id="text" rows="2" required>In one sentence, what is a tmux session?</textarea>70    <button id="go" class="primary" type="submit">Send</button>71  </form>72  <div id="out"></div>73  <div id="state"></div>74</main>75<script>76const $ = (id) => document.getElementById(id);77const capsuleUrl = location.origin + location.pathname.replace(/\/human\/.*$/, "");78const base = capsuleUrl + "/v1";79$("base").textContent = base;80$("login").textContent = "opencode auth login " + capsuleUrl;81const provider = {82  dsflash: {83    npm: "@ai-sdk/openai-compatible",84    name: "DeepSeek V4.1 Flash (capsule)",85    options: { baseURL: base },86    models: { "deepseek-v4.1-flash": { name: "DeepSeek V4.1 Flash", tool_call: true } },87  },88};89const model = "dsflash/deepseek-v4.1-flash";90$("config").textContent = JSON.stringify({ "$schema": "https://opencode.ai/config.json", provider, model }, null, 2);91// Single-quoted for the shell: the JSON has no single quotes (the URL comes from location).92$("run").textContent = "OPENCODE_CONFIG_CONTENT='" + JSON.stringify({ provider, model }) + "' opencode";9394fetch("../status").then((r) => r.json()).then((s) => {95  $("phase").textContent = s.phase === "ready" ? "ready" : s.phase + (s.detail ? " (" + s.detail + ")" : "");96}).catch(() => { $("phase").textContent = "status unavailable"; });9798for (const b of document.querySelectorAll("[data-copy]")) {99  b.addEventListener("click", async () => {100    const text = $(b.dataset.copy).textContent;101    try { await navigator.clipboard.writeText(text); }102    catch (e) {  // plain http is not a secure context: select and copy the old way103      const range = document.createRange();104      range.selectNodeContents($(b.dataset.copy));105      getSelection().removeAllRanges();106      getSelection().addRange(range);107      document.execCommand("copy");108    }109    b.textContent = "copied";110    setTimeout(() => { b.textContent = "copy"; }, 1500);111  });112}113114// Streams the reply with the same SSE route opencode uses.115$("f").addEventListener("submit", async (ev) => {116  ev.preventDefault();117  const text = $("text").value.trim();118  if (!text) return;119  $("go").disabled = true;120  $("out").textContent = "";121  $("out").className = "";122  const t0 = performance.now();123  let first = null;124  $("state").textContent = "waiting…";125  try {126    const r = await fetch("../v1/chat/completions", {127      method: "POST", headers: { "Content-Type": "application/json" },128      body: JSON.stringify({ messages: [{ role: "user", content: text }], stream: true, max_tokens: 800 }),129    });130    if (!r.ok || !r.body) {131      const d = await r.text();132      $("out").className = "err";133      $("out").textContent = "HTTP " + r.status + ": " + d;134      return;135    }136    const reader = r.body.getReader();137    const dec = new TextDecoder();138    let buf = "";139    for (;;) {140      const { value, done } = await reader.read();141      if (done) break;142      buf += dec.decode(value, { stream: true });143      let i;144      while ((i = buf.indexOf("\n")) >= 0) {145        const line = buf.slice(0, i).trim();146        buf = buf.slice(i + 1);147        if (!line.startsWith("data:")) continue;148        const data = line.slice(5).trim();149        if (data === "[DONE]") continue;150        try {151          const d = JSON.parse(data);152          if (d.error) { $("out").className = "err"; $("out").textContent += "\n" + JSON.stringify(d.error); continue; }153          const piece = d.choices && d.choices[0] && d.choices[0].delta && d.choices[0].delta.content;154          if (piece) {155            if (first === null) first = performance.now();156            $("out").textContent += piece;157          }158        } catch (e) { /* partial or non-JSON line */ }159      }160      $("state").textContent = "streaming… " + ((performance.now() - t0) / 1000).toFixed(1) + " s";161    }162    const total = ((performance.now() - t0) / 1000).toFixed(1);163    $("state").textContent = "done in " + total + " s" + (first ? " (first token " + ((first - t0) / 1000).toFixed(1) + " s)" : "");164  } catch (e) {165    $("out").className = "err";166    $("out").textContent = "Request failed: " + e;167  } finally {168    $("go").disabled = false;169  }170});171</script>172</html>