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>