Spaces:
Running
Running
Download app.js from xdLocutus/Smaug-Flash-Explorer: direct link, hf CLI and curl.
- Browser
- Download file 35 kB
-
https://huggingface.co/spaces/xdLocutus/Smaug-Flash-Explorer/resolve/main/app.js
- Command line
-
hf download hf://spaces/xdLocutus/Smaug-Flash-Explorer/app.js
-
curl -L -o app.js https://huggingface.co/spaces/xdLocutus/Smaug-Flash-Explorer/resolve/main/app.js
35 kB
| /* Smaug-Flash Explorer — static Space logic. | |
| * No backend: benchmarks are the model card's numbers, the encoder runs the | |
| * model repo's own encoding_dsv4.py inside Pyodide, and the chat client speaks | |
| * OpenAI-compatible SSE straight to an endpoint you choose. | |
| */ | |
| ; | |
| const MODEL_ID = 'abacusai/Smaug-Flash'; | |
| const BASE_ID = 'deepseek-ai/DeepSeek-V4-Flash-0731'; | |
| const MODEL_URL = 'https://huggingface.co/' + MODEL_ID; | |
| const PYODIDE_VERSION = 'v314.0.7'; // pyodide release tag on jsdelivr | |
| // ---- model facts (config.json + Hub safetensors metadata) ------------------- | |
| const N_LAYERS = 43, N_ROUTED = 256, N_SHARED = 1, MOE_INTER = 2048, HIDDEN = 4096; | |
| const MAX_POSITIONS = 1048576; | |
| const TOTAL_PARAMS = 304180418494; // 304.18 B | |
| const EXPERT_PARAMS = N_LAYERS * (N_ROUTED + N_SHARED) * 3 * HIDDEN * MOE_INTER; | |
| const OTHER_PARAMS = TOTAL_PARAMS - EXPERT_PARAMS; | |
| const CHECKPOINT_GB = 166.89; | |
| // ---- benchmark data (digitized from the model card's figures) -------------- | |
| const BENCHMARKS = [ | |
| { name: 'LiveBench overall-7', smaug: 77.4, base: 74.2, starred: false }, | |
| { name: 'LiveBench agentic coding', smaug: 61.1, base: 46.8, starred: false }, | |
| { name: 'Terminal Bench 2.1 (DeepSeek harness)', smaug: 76.4, base: 66.3, starred: false }, | |
| { name: 'Terminal Bench 2.1 (terminus-2)', smaug: 70.8, base: 68.2, starred: false }, | |
| { name: 'AutomationBench Public', smaug: 38.8, base: 25.1, starred: true }, | |
| { name: 'NL2Repo-Bench', smaug: 73.3, base: 54.2, starred: true }, | |
| { name: 'DeepSWE v1.1', smaug: 56.6, base: 54.4, starred: true }, | |
| { name: 'AA-LCR (long-context, 100 q)', smaug: 74.0, base: 75.5, starred: false } | |
| ]; | |
| const LIVEBENCH = [ | |
| { name: 'Agentic Coding', smaug: 61.1, base: 46.8 }, | |
| { name: 'Instruction Following', smaug: 69.3, base: 65.5 }, | |
| { name: 'Mathematics', smaug: 90.1, base: 86.8 }, | |
| { name: 'Coding', smaug: 77.1, base: 75.0 }, | |
| { name: 'Language', smaug: 79.2, base: 79.2 }, | |
| { name: 'Data Analysis', smaug: 79.1, base: 79.3 }, | |
| { name: 'Reasoning', smaug: 86.2, base: 86.6 }, | |
| { name: 'Overall', smaug: 77.4, base: 74.2 } | |
| ]; | |
| const HARDWARE = [ | |
| { name: 'cpu-basic', note: '2 vCPU / 16 GB — no GPU', gpu: 0, usd: 0 }, | |
| { name: 'zero-a10g (ZeroGPU)', note: '96 GB full MIG, dynamic — this Space', gpu: 96, usd: 0 }, | |
| { name: 't4-small', note: '1× T4 16 GB', gpu: 16, usd: 0.40 }, | |
| { name: 'a10g-largex2', note: '2× A10G', gpu: 48, usd: 3.00 }, | |
| { name: 'a10g-largex4', note: '4× A10G', gpu: 96, usd: 5.00 }, | |
| { name: 'a100-large', note: '1× A100 80 GB', gpu: 80, usd: 2.50 }, | |
| { name: 'a100x4', note: '4× A100 80 GB', gpu: 320, usd: 10.00 }, | |
| { name: 'a100x8', note: '8× A100 80 GB', gpu: 640, usd: 20.00 }, | |
| { name: 'rtx-pro-6000x2', note: '2× RTX PRO 6000 96 GB', gpu: 192, usd: 5.50 }, | |
| { name: 'rtx-pro-6000x4', note: '4× RTX PRO 6000 96 GB', gpu: 384, usd: 11.00 }, | |
| { name: 'rtx-pro-6000x8', note: '8× RTX PRO 6000 96 GB', gpu: 768, usd: 22.00 }, | |
| { name: 'l40sx8', note: '8× L40S 48 GB', gpu: 384, usd: 23.50 } | |
| ]; | |
| const RECIPES = { | |
| 'sglang-cmd': `sglang serve \\ | |
| --trust-remote-code \\ | |
| --model-path abacusai/Smaug-Flash \\ | |
| --tp 4 \\ | |
| --enable-dp-attention \\ | |
| --moe-runner-backend flashinfer_mxfp4 \\ | |
| --speculative-algorithm DSPARK \\ | |
| --mem-fraction-static 0.90 \\ | |
| --chunked-prefill-size 4096 \\ | |
| --swa-full-tokens-ratio 0.1`, | |
| 'vllm-cmd': `vllm serve abacusai/Smaug-Flash \\ | |
| --trust-remote-code --kv-cache-dtype fp8 --block-size 256 \\ | |
| --data-parallel-size 4 --enable-expert-parallel \\ | |
| --moe-backend deep_gemm_mega_moe \\ | |
| --attention-config '{"use_fp4_indexer_cache": true}' \\ | |
| --speculative-config '{"method":"dspark","num_speculative_tokens":7,"draft_sample_method":"greedy"}'`, | |
| 'torch-cmd': `export EXPERTS=256 | |
| export MP=4 | |
| export CONFIG=config.json | |
| python convert.py --hf-ckpt-path \${HF_CKPT_PATH} --save-path \${SAVE_PATH} \\ | |
| --n-experts \${EXPERTS} --model-parallel \${MP} | |
| torchrun --nproc-per-node \${MP} generate.py --ckpt-path \${SAVE_PATH} --config \${CONFIG} --interactive`, | |
| 'py-cmd': `from encoding_dsv4 import encode_messages, parse_message_from_completion_text | |
| from openai import OpenAI | |
| messages = [{"role": "user", "content": "Write a Python one-liner that reverses a string."}] | |
| prompt = encode_messages(messages, thinking_mode="thinking", reasoning_effort="max") | |
| client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY") | |
| out = client.completions.create(model="abacusai/Smaug-Flash", prompt=prompt, max_tokens=8192) | |
| print(parse_message_from_completion_text(out.choices[0].text, thinking_mode="thinking"))` | |
| }; | |
| const EXAMPLES = { | |
| chat: [ | |
| { role: 'system', content: 'You are a helpful assistant.' }, | |
| { role: 'user', content: 'What is 2+2? Think it through.' } | |
| ], | |
| agentic: [ | |
| { role: 'system', content: 'You are an autonomous coding agent. Use the tools you are given.' }, | |
| { role: 'user', content: 'Add a --verbose flag to cmd/cli.py and run the tests.' }, | |
| { | |
| role: 'assistant', | |
| content: "I'll read the file first.", | |
| reasoning_content: 'Need the current CLI definition before editing.', | |
| tool_calls: [{ id: 'call_1', type: 'function', function: { name: 'read_file', arguments: { path: 'cmd/cli.py' } } }] | |
| }, | |
| { role: 'tool', tool_call_id: 'call_1', content: 'def main():\n pass\n' }, | |
| { role: 'user', content: 'Also keep the exit code at 0 on success.' } | |
| ], | |
| custom: [ | |
| { role: 'system', content: 'You are a senior Python reviewer. Be terse.' }, | |
| { role: 'latest_reminder', content: 'Date: 2026-10-31, locale: en-ZA, timezone: UTC+02:00' }, | |
| { role: 'user', content: 'Review this diff:\n- return x\n+ return x or 0' } | |
| ] | |
| }; | |
| const EXAMPLE_TOOLS = [ | |
| { | |
| type: 'function', | |
| function: { | |
| name: 'read_file', | |
| description: 'Read a UTF-8 text file from the repo.', | |
| parameters: { type: 'object', properties: { path: { type: 'string', description: 'repo-relative path' } }, required: ['path'] } | |
| } | |
| } | |
| ]; | |
| // ============================================================ small helpers | |
| const $ = (id) => document.getElementById(id); | |
| function escapeHtml(s) { | |
| return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); | |
| } | |
| function gb(bytes) { return (bytes / 1e9).toFixed(1) + ' GB'; } | |
| // ============================================================ tab switching | |
| document.querySelectorAll('.tab').forEach((btn) => { | |
| btn.addEventListener('click', () => showTab(btn.dataset.tab)); | |
| }); | |
| function showTab(name) { | |
| document.querySelectorAll('.tab').forEach((b) => b.classList.toggle('active', b.dataset.tab === name)); | |
| document.querySelectorAll('.panel').forEach((p) => p.classList.toggle('active', p.id === 'tab-' + name)); | |
| if (showTab._mounted === undefined) showTab._mounted = {}; | |
| if (!showTab._mounted[name]) { | |
| showTab._mounted[name] = true; | |
| if (name === 'benchmarks') { drawBenchCharts(); } | |
| if (name === 'deploy') { drawSizeChart(); } | |
| if (name === 'encoder') { mountEncoder(); } | |
| } | |
| } | |
| document.querySelectorAll('[data-goto]').forEach((a) => { | |
| a.addEventListener('click', (e) => { e.preventDefault(); showTab(a.dataset.goto); }); | |
| }); | |
| // ============================================================ live Hub stats | |
| (async function hubStats() { | |
| const el = $('hub-live'); | |
| try { | |
| const info = await (await fetch('https://huggingface.co/api/models/' + MODEL_ID)).json(); | |
| el.textContent = 'Live Hub stats: ' + (info.downloads ?? 0).toLocaleString() + ' downloads · ' + | |
| (info.likes ?? 0).toLocaleString() + ' likes · last modified ' + | |
| String(info.lastModified || '').slice(0, 10) + ' · sha ' + String(info.sha || '').slice(0, 8) + | |
| ' · license ' + ((info.cardData && info.cardData.license) || 'mit'); | |
| } catch (err) { | |
| el.textContent = 'Live Hub stats unavailable offline — the numbers in this page were captured from the model card.'; | |
| } | |
| })(); | |
| // ============================================================ charts | |
| const PLOTLY_LAYOUT = { | |
| paper_bgcolor: 'rgba(0,0,0,0)', | |
| plot_bgcolor: 'rgba(0,0,0,0)', | |
| font: { color: '#e8ecf8' }, | |
| margin: { l: 10, r: 10, t: 50, b: 40 } | |
| }; | |
| function drawBenchCharts() { | |
| const labels = BENCHMARKS.map((b) => b.name + ' (' + (b.smaug - b.base >= 0 ? '+' : '') + (b.smaug - b.base).toFixed(1) + ')'); | |
| Plotly.newPlot('chart-bars', [ | |
| { | |
| type: 'bar', orientation: 'h', name: 'Smaug-Flash', y: labels, x: BENCHMARKS.map((b) => b.smaug), | |
| marker: { color: '#6d7cff' }, text: BENCHMARKS.map((b) => b.smaug.toFixed(1)), textposition: 'outside', | |
| hovertemplate: '%{y}<br>Smaug-Flash: %{x:.1f}<extra></extra>' | |
| }, | |
| { | |
| type: 'bar', orientation: 'h', name: BASE_ID + ' (base)', y: labels, x: BENCHMARKS.map((b) => b.base), | |
| marker: { color: '#f59e0b' }, text: BENCHMARKS.map((b) => b.base.toFixed(1) + (b.starred ? '*' : '')), | |
| textposition: 'outside', hovertemplate: '%{y}<br>base: %{x:.1f}<extra></extra>' | |
| } | |
| ], Object.assign({}, PLOTLY_LAYOUT, { | |
| title: 'Smaug-Flash vs DeepSeek-V4-Flash-0731 (Δ in the axis label)', | |
| barmode: 'group', height: 520, legend: { orientation: 'h' }, xaxis: { title: 'score (higher is better)' } | |
| }), { responsive: true, displayModeBar: false }); | |
| Plotly.newPlot('chart-delta', [ | |
| { | |
| type: 'bar', orientation: 'h', y: BENCHMARKS.map((b) => b.name), x: BENCHMARKS.map((b) => b.smaug - b.base), | |
| marker: { color: BENCHMARKS.map((b) => (b.smaug - b.base >= 0 ? '#34d399' : '#f87171')) }, | |
| text: BENCHMARKS.map((b) => (b.smaug - b.base >= 0 ? '+' : '') + (b.smaug - b.base).toFixed(1)), | |
| textposition: 'outside', hovertemplate: '%{y}: %{x:+.1f}<extra></extra>' | |
| } | |
| ], Object.assign({}, PLOTLY_LAYOUT, { | |
| title: 'Δ vs base — 7 of 8 up, AA-LCR down 1.5', height: 520, xaxis: { title: 'points' }, showlegend: false | |
| }), { responsive: true, displayModeBar: false }); | |
| const cats = LIVEBENCH.filter((c) => c.name !== 'Overall'); | |
| const theta = cats.map((c) => c.name).concat([cats[0].name]); | |
| Plotly.newPlot('chart-radar', [ | |
| { | |
| type: 'scatterpolar', r: cats.map((c) => c.smaug).concat([cats[0].smaug]), theta: theta, | |
| fill: 'toself', name: 'Smaug-Flash', line: { color: '#6d7cff' } | |
| }, | |
| { | |
| type: 'scatterpolar', r: cats.map((c) => c.base).concat([cats[0].base]), theta: theta, | |
| fill: 'toself', name: 'DSV4-Flash-0731 (base)', line: { color: '#f59e0b' }, opacity: 0.65 | |
| } | |
| ], Object.assign({}, PLOTLY_LAYOUT, { | |
| title: 'LiveBench 2026-06-25 categories (0–100)', height: 520, legend: { orientation: 'h' }, | |
| polar: { radialaxis: { visible: true, range: [40, 100], ticks: '' } } | |
| }), { responsive: true, displayModeBar: false }); | |
| const tbody = document.querySelector('#bench-table tbody'); | |
| tbody.innerHTML = BENCHMARKS.map((b) => { | |
| const d = b.smaug - b.base; | |
| return '<tr><td>' + escapeHtml(b.name) + '</td><td>' + b.smaug.toFixed(1) + '</td><td>' + | |
| b.base.toFixed(1) + (b.starred ? '*' : '') + '</td><td class="' + (d >= 0 ? 'badge-ok' : 'badge-bad') + '">' + | |
| (d >= 0 ? '+' : '') + d.toFixed(1) + '</td></tr>'; | |
| }).join(''); | |
| const lbody = document.querySelector('#lb-table tbody'); | |
| lbody.innerHTML = LIVEBENCH.map((c) => { | |
| const d = c.smaug - c.base; | |
| return '<tr><td>' + escapeHtml(c.name) + '</td><td>' + c.smaug.toFixed(1) + '</td><td>' + c.base.toFixed(1) + | |
| '</td><td class="' + (d > 0 ? 'badge-ok' : d < 0 ? 'badge-bad' : '') + '">' + (d === 0 ? 'par' : (d > 0 ? '+' : '') + d.toFixed(1)) + '</td></tr>'; | |
| }).join(''); | |
| } | |
| showTab._mounted = { overview: true }; | |
| // ============================================================ sizing calculator | |
| function computeSize(attnBpe, expertBpe, headroomPct) { | |
| const expertGb = EXPERT_PARAMS * expertBpe / 1e9; | |
| const otherGb = OTHER_PARAMS * attnBpe / 1e9; | |
| const weightsGb = expertGb + otherGb; | |
| const totalGb = weightsGb * (1 + headroomPct / 100); | |
| return { expertGb, otherGb, weightsGb, totalGb }; | |
| } | |
| function updateSize() { | |
| const attnBpe = parseFloat($('attn-bits').value); | |
| const expertBpe = parseFloat($('expert-bits').value); | |
| const headroom = parseFloat($('headroom').value); | |
| $('headroom-v').textContent = String(headroom); | |
| const s = computeSize(attnBpe, expertBpe, headroom); | |
| $('size-out').innerHTML = | |
| '<table class="kv">' + | |
| '<tr><td>Expert FFNs (43 layers × 257 experts × 3 matrices × 4096 × 2048)</td><td>' + | |
| (EXPERT_PARAMS / 1e9).toFixed(1) + ' B params → <strong>' + s.expertGb.toFixed(1) + ' GB</strong></td></tr>' + | |
| '<tr><td>Attention + embeddings + DSpark</td><td>' + (OTHER_PARAMS / 1e9).toFixed(1) + | |
| ' B params → <strong>' + s.otherGb.toFixed(1) + ' GB</strong></td></tr>' + | |
| '<tr><td>Weights subtotal</td><td><strong>' + s.weightsGb.toFixed(1) + ' GB</strong></td></tr>' + | |
| '<tr><td>+ ' + headroom + '% headroom (KV cache, activations, overhead)</td><td>+' + | |
| (s.totalGb - s.weightsGb).toFixed(1) + ' GB</td></tr>' + | |
| '<tr><td>Budget to fit</td><td><strong>' + s.totalGb.toFixed(1) + ' GB</strong></td></tr>' + | |
| '</table>' + | |
| '<p class="note">Reference: the shipped checkpoint is ' + CHECKPOINT_GB + ' GB on the Hub — block-FP8 ' + | |
| 'attention × packed-FP4 experts (' + computeSize(1, 0.5, 0).weightsGb.toFixed(1) + | |
| ' GB here, the rest is index/tokenizer overhead and the BF16/FP8 mix).</p>'; | |
| const rows = HARDWARE.slice().sort((a, b) => a.usd - b.usd).map((hw) => { | |
| let verdict; | |
| if (hw.gpu <= 0) verdict = '— no GPU'; | |
| else if (hw.gpu >= s.totalGb) verdict = '<span class="badge-ok">✅ fits on paper</span>'; | |
| else verdict = '<span class="badge-bad">❌ short by ' + (s.totalGb - hw.gpu).toFixed(0) + ' GB</span>'; | |
| return '<tr><td>' + escapeHtml(hw.name) + '</td><td>' + escapeHtml(hw.note) + '</td><td>' + hw.gpu + | |
| ' GB</td><td>' + (hw.usd === 0 ? 'free' : '$' + hw.usd.toFixed(2) + '/h') + '</td><td>' + verdict + '</td></tr>'; | |
| }).join(''); | |
| $('hw-table').querySelector('tbody').innerHTML = rows; | |
| $('chart-size').dataset.budget = String(s.totalGb); | |
| drawSizeChart(); | |
| } | |
| function drawSizeChart() { | |
| const el = $('chart-size'); | |
| if (!el || !el.dataset.budget) return; | |
| const attnBpe = parseFloat($('attn-bits').value); | |
| const expertBpe = parseFloat($('expert-bits').value); | |
| const headroom = parseFloat($('headroom').value); | |
| const s = computeSize(attnBpe, expertBpe, headroom); | |
| Plotly.newPlot('chart-size', [ | |
| { | |
| type: 'bar', x: ['weights', '+' + headroom + '% headroom'], | |
| y: [s.weightsGb, s.totalGb - s.weightsGb], marker: { color: ['#6d7cff', '#a5b4fc'] }, | |
| text: [s.weightsGb.toFixed(0) + ' GB', (s.totalGb - s.weightsGb).toFixed(0) + ' GB'], | |
| textposition: 'inside', hovertemplate: '%{x}: %{y:.1f} GB<extra></extra>' | |
| } | |
| ], Object.assign({}, PLOTLY_LAYOUT, { | |
| title: 'Budget vs the 96 GB ZeroGPU ceiling', height: 420, showlegend: false, yaxis: { title: 'GB (decimal)' }, | |
| shapes: [{ type: 'line', x0: -0.5, x1: 1.5, y0: 96, y1: 96, line: { color: '#f87171', dash: 'dash', width: 2 } }], | |
| annotations: [{ x: 1, y: 96, text: 'ZeroGPU xlarge 96 GB', showarrow: false, yshift: 10 }] | |
| }), { responsive: true, displayModeBar: false }); | |
| } | |
| ['attn-bits', 'expert-bits', 'headroom'].forEach((id) => { | |
| const el = $(id); | |
| el.addEventListener('input', updateSize); | |
| el.addEventListener('change', updateSize); | |
| }); | |
| // ============================================================ recipes + copy | |
| Object.keys(RECIPES).forEach((id) => { | |
| const pre = $(id); | |
| if (pre) pre.textContent = RECIPES[id]; | |
| }); | |
| document.querySelectorAll('button.copy').forEach((btn) => { | |
| btn.addEventListener('click', async () => { | |
| const pre = $(btn.dataset.copy); | |
| if (!pre) return; | |
| try { | |
| await navigator.clipboard.writeText(pre.textContent); | |
| btn.textContent = 'copied'; | |
| } catch (err) { | |
| btn.textContent = 'select + Ctrl-C'; | |
| } | |
| setTimeout(() => { btn.textContent = 'copy'; }, 1600); | |
| }); | |
| }); | |
| // ============================================================ Pyodide encoder | |
| const PY_SETUP = ` | |
| import sys, json | |
| sys.path.insert(0, "/home/pyodide") | |
| import encoding_dsv4 as _dsv4 | |
| def smaug_encode(args_json): | |
| try: | |
| args = json.loads(args_json) | |
| messages = args.get("messages") or [] | |
| tools = args.get("tools") or None | |
| if tools and messages: | |
| messages = [dict(m) for m in messages] | |
| messages[0] = dict(messages[0], tools=tools) | |
| prompt = _dsv4.encode_messages( | |
| messages, | |
| thinking_mode=args["thinking"], | |
| reasoning_effort=(args.get("effort") or None), | |
| drop_thinking=bool(args.get("drop", True)), | |
| ) | |
| return json.dumps({"ok": True, "prompt": prompt, "n_messages": len(messages)}) | |
| except Exception as exc: | |
| return json.dumps({"ok": False, "error": type(exc).__name__ + ": " + str(exc)}) | |
| def smaug_decode(args_json): | |
| try: | |
| args = json.loads(args_json) | |
| parsed = _dsv4.parse_message_from_completion_text(args["text"], thinking_mode=args["thinking"]) | |
| return json.dumps({"ok": True, "parsed": parsed}) | |
| except Exception as exc: | |
| return json.dumps({"ok": False, "error": type(exc).__name__ + ": " + str(exc)}) | |
| def smaug_effort_sizes(): | |
| return json.dumps({k: len(v) for k, v in _dsv4.REASONING_EFFORT_PROMPTS.items()}) | |
| def smaug_tokens(): | |
| return json.dumps({ | |
| "bos": _dsv4.bos_token, | |
| "eos": _dsv4.eos_token, | |
| "think_start": _dsv4.thinking_start_token, | |
| "think_end": _dsv4.thinking_end_token, | |
| "dsml": _dsv4.dsml_token, | |
| }) | |
| def smaug_repair_delimiters(args_json): | |
| # The repo delimits special tokens with U+FF5C, not ASCII '|'. Repair pipes | |
| # typed by hand (or copied from a rendering of the model card), and report it. | |
| import re as _re | |
| text = json.loads(args_json)["text"] | |
| repaired = _re.sub(r"<\|([^|<>{]{0,24})\|>", lambda m: "<" + chr(0xff5c) + m.group(1) + chr(0xff5c) + ">", text) | |
| return json.dumps({"text": repaired, "repaired": repaired != text}) | |
| `; | |
| let pyodidePromise = null; | |
| const DEFAULT_COMPLETION = document.getElementById('completion-in') ? document.getElementById('completion-in').value : ''; | |
| function setPyStatus(text, cls) { | |
| const el = $('py-status'); | |
| if (!el) return; | |
| el.textContent = text; | |
| el.className = 'status' + (cls ? ' ' + cls : ''); | |
| } | |
| function loadScript(src) { | |
| return new Promise((resolve, reject) => { | |
| const s = document.createElement('script'); | |
| s.src = src; s.onload = resolve; s.onerror = () => reject(new Error('failed to load ' + src)); | |
| document.head.appendChild(s); | |
| }); | |
| } | |
| async function loadPyodideRuntime(indexURL) { | |
| // Prefer the ES module build. Older releases only expose a `loadPyodide` | |
| // global from the classic script, so keep that as the fallback. | |
| try { | |
| const mod = await import(indexURL + 'pyodide.mjs'); | |
| if (typeof mod.loadPyodide === 'function') return await mod.loadPyodide({ indexURL }); | |
| } catch (err) { | |
| console.warn('pyodide.mjs unavailable, falling back to classic pyodide.js:', err); | |
| } | |
| await loadScript(indexURL + 'pyodide.js'); | |
| if (typeof window.loadPyodide !== 'function') { | |
| throw new Error('neither pyodide.mjs nor a loadPyodide global is available at ' + indexURL); | |
| } | |
| return await window.loadPyodide({ indexURL }); | |
| } | |
| async function ensurePyodide() { | |
| if (pyodidePromise) return pyodidePromise; | |
| pyodidePromise = (async () => { | |
| setPyStatus('loading Python in your browser (≈ 10 MB, one time)...'); | |
| const indexURL = 'https://cdn.jsdelivr.net/pyodide/' + PYODIDE_VERSION + '/full/'; | |
| const py = await loadPyodideRuntime(indexURL); | |
| const src = await (await fetch('encoding_dsv4.py')).text(); | |
| py.FS.writeFile('/home/pyodide/encoding_dsv4.py', src); | |
| await py.runPythonAsync(PY_SETUP); | |
| const sizes = JSON.parse(py.runPython('smaug_effort_sizes()')); | |
| window.__smaugTokens = JSON.parse(py.runPython('smaug_tokens()')); | |
| const box = $('completion-in'); | |
| if (box && box.value === DEFAULT_COMPLETION) { | |
| box.value = 'Let me add the two integers.' + window.__smaugTokens.think_end + | |
| 'The answer is 4.' + window.__smaugTokens.eos; | |
| } | |
| setPyStatus('runtime ready — encodes with the model repo’s own encoding_dsv4.py (' + | |
| sizes.low + '/' + sizes.high + '/' + sizes.max + ' char preambles for low/high/max)', 'badge-ok'); | |
| return py; | |
| })(); | |
| try { | |
| return await pyodidePromise; | |
| } catch (err) { | |
| pyodidePromise = null; | |
| setPyStatus('⚠️ Python runtime failed to load: ' + err.message + ' — check the browser console.', 'badge-bad'); | |
| throw err; | |
| } | |
| } | |
| function setMessagesExample(which) { | |
| $('messages-in').value = JSON.stringify(EXAMPLES[which], null, 2); | |
| if (which === 'agentic') $('tools-in').value = JSON.stringify(EXAMPLE_TOOLS, null, 2); | |
| else if ($('tools-in').value.trim() === JSON.stringify(EXAMPLE_TOOLS, null, 2)) $('tools-in').value = ''; | |
| } | |
| function mountEncoder() { | |
| if (!$('messages-in').value.trim()) setMessagesExample('chat'); | |
| if (!$('tools-in').value.trim()) $('tools-in').value = ''; | |
| } | |
| (async function initEncoder() { | |
| setMessagesExample('chat'); | |
| $('tools-in').value = ''; | |
| document.querySelectorAll('[data-example]').forEach((b) => { | |
| b.addEventListener('click', () => setMessagesExample(b.dataset.example)); | |
| }); | |
| $('encode-btn').addEventListener('click', async () => { | |
| const out = $('prompt-out'), stats = $('encode-stats'), notes = $('encode-notes'); | |
| out.value = ''; stats.textContent = ''; notes.innerHTML = ''; | |
| let messages, tools = null; | |
| try { | |
| messages = JSON.parse($('messages-in').value || '[]'); | |
| } catch (err) { | |
| stats.innerHTML = '<span class="badge-bad">⚠️ messages JSON is invalid: ' + escapeHtml(err.message) + '</span>'; | |
| return; | |
| } | |
| if (!Array.isArray(messages) || messages.length === 0) { | |
| stats.innerHTML = '<span class="badge-bad">⚠️ provide a non-empty JSON array of messages.</span>'; | |
| return; | |
| } | |
| const toolsRaw = $('tools-in').value.trim(); | |
| if (toolsRaw) { | |
| try { tools = JSON.parse(toolsRaw); } | |
| catch (err) { | |
| stats.innerHTML = '<span class="badge-bad">⚠️ tools JSON is invalid: ' + escapeHtml(err.message) + '</span>'; | |
| return; | |
| } | |
| } | |
| const thinking = $('thinking-in').value; | |
| const effort = $('effort-in').value; | |
| const drop = $('drop-in').checked; | |
| let py; | |
| try { py = await ensurePyodide(); } catch (err) { return; } | |
| py.globals.set('ARGS_JSON', JSON.stringify({ messages, tools, thinking, effort, drop })); | |
| const result = JSON.parse(py.runPython('smaug_encode(ARGS_JSON)')); | |
| if (!result.ok) { | |
| stats.innerHTML = '<span class="badge-bad">⚠️ encoding failed — <code>' + escapeHtml(result.error) + '</code></span>'; | |
| return; | |
| } | |
| const prompt = result.prompt; | |
| out.value = prompt; | |
| const dropNote = thinking === 'thinking' | |
| ? (drop ? 'Dropped' : 'Kept') + ' reasoning_content on assistant turns before the last user message.' | |
| : 'chat mode: no thinking blocks are rendered at all.'; | |
| stats.innerHTML = 'messages in: <strong>' + result.n_messages + '</strong> · chars: <strong>' + | |
| prompt.length.toLocaleString() + '</strong> · thinking_mode: <code>' + thinking + | |
| '</code> · reasoning_effort: <code>' + effort + '</code>' + | |
| (tools ? ' · tools rendered: <strong>' + tools.length + '</strong>' : ' · no tools'); | |
| notes.innerHTML = | |
| '<ol>' + | |
| '<li>Merged <code>tool</code> messages into the preceding user turn and sorted tool results by ' + | |
| 'call order — DeepSeek-V4 has no separate tool role slot in the prompt ' + | |
| '(<code>merge_tool_messages()</code> / <code>sort_tool_results_by_call_order()</code>).</li>' + | |
| '<li>' + escapeHtml(dropNote) + '</li>' + | |
| '<li>Prepended the BOS token and closed the final assistant turn with <code><|Assistant|></code>' + | |
| (thinking === 'thinking' ? ' plus <code><think></code>.' : ' plus an immediate <code></think></code>.') + '</li>' + | |
| '<li>Tokenize this string exactly as-is: <code>tokenizer.encode(prompt)</code> — no chat template, ' + | |
| 'no extra separators.</li>' + | |
| '</ol>'; | |
| }); | |
| $('decode-btn').addEventListener('click', async () => { | |
| const status = $('decode-status'); | |
| const completion = $('completion-in').value; | |
| if (!completion.trim()) { status.innerHTML = '<span class="badge-bad">⚠️ paste a completion first.</span>'; return; } | |
| let py; | |
| try { py = await ensurePyodide(); } catch (err) { return; } | |
| py.globals.set('ARGS_JSON', JSON.stringify({ text: completion })); | |
| const repair = JSON.parse(py.runPython('smaug_repair_delimiters(ARGS_JSON)')); | |
| if (repair.repaired) { | |
| $('completion-in').value = repair.text; | |
| } | |
| const repairNote = repair.repaired | |
| ? ' <span class="note">(repaired ASCII <code>|</code> to the repo’s U+FF5C delimiter)</span>' : ''; | |
| py.globals.set('ARGS_JSON', JSON.stringify({ text: repair.text, thinking: $('decode-mode').value })); | |
| const result = JSON.parse(py.runPython('smaug_decode(ARGS_JSON)')); | |
| if (!result.ok) { | |
| $('parsed-out').value = ''; | |
| status.innerHTML = '<span class="badge-bad">⚠️ parse failed — <code>' + escapeHtml(result.error) + | |
| '</code></span><br><span class="note">The parser only accepts well-formed output: close the think ' + | |
| 'block with <code></think></code> and end the turn with <code><|end▁of▁sentence|></code>.</span>'; | |
| return; | |
| } | |
| $('parsed-out').value = JSON.stringify(result.parsed, null, 2); | |
| status.innerHTML = '<span class="badge-ok">✅ parsed cleanly into OpenAI-format fields</span>' + repairNote; | |
| }); | |
| })(); | |
| // ============================================================ playground | |
| const PRESETS = { | |
| own: { | |
| baseUrl: 'http://your-host:8000/v1', | |
| model: MODEL_ID, | |
| key: 'EMPTY', | |
| hint: 'vLLM and SGLang both expose an OpenAI-compatible /v1 endpoint. The host must be reachable from ' + | |
| 'this browser: a laptop localhost is not, unless you opened this page locally or tunnelled it.' | |
| }, | |
| router: { | |
| baseUrl: 'https://router.huggingface.co/v1', | |
| model: 'deepseek-ai/DeepSeek-V4.1-Flash-1017', | |
| key: '', | |
| hint: 'Live and OpenAI-compatible, but the router does not serve Smaug-Flash — use it to sanity-check the ' + | |
| 'plumbing against a supported Flash-class model, then switch to your own deployment for the real thing. ' + | |
| 'Paste an HF token with Inference Providers permission as the key.' | |
| }, | |
| local: { | |
| baseUrl: 'http://127.0.0.1:11434/v1', | |
| model: 'smaug-flash', | |
| key: 'ollama', | |
| hint: 'Only resolves if this page runs on the same machine as the runtime (serve the repo folder locally).' | |
| }, | |
| custom: { baseUrl: '', model: MODEL_ID, key: '', hint: 'Any OpenAI-compatible /v1 base URL works.' } | |
| }; | |
| const chatHistory = []; | |
| let streaming = false; | |
| function applyPreset() { | |
| const p = PRESETS[$('preset').value] || PRESETS.own; | |
| $('base-url').value = p.baseUrl; | |
| $('model-name').value = p.model; | |
| $('api-key').value = p.key; | |
| $('preset-hint').textContent = p.hint; | |
| } | |
| function renderChat() { | |
| const log = $('chat-log'); | |
| if (!chatHistory.length) { | |
| log.innerHTML = '<p class="note">Nothing here yet — set an endpoint, then send a message.</p>'; | |
| return; | |
| } | |
| log.innerHTML = chatHistory.map((m) => { | |
| const cls = m.role === 'user' ? 'user' : (m.error ? 'error' : 'assistant'); | |
| return '<div class="msg ' + cls + '"><span class="who">' + m.role + '</span>' + escapeHtml(m.content) + '</div>'; | |
| }).join(''); | |
| log.scrollTop = log.scrollHeight; | |
| } | |
| ['pg-temp', 'pg-topp'].forEach((id) => { | |
| const el = $(id); | |
| el.addEventListener('input', () => { $('pg-temp-v').textContent = parseFloat($('pg-temp').value).toFixed(2); | |
| $('pg-topp-v').textContent = parseFloat($('pg-topp').value).toFixed(2); }); | |
| }); | |
| $('preset').addEventListener('change', applyPreset); | |
| applyPreset(); | |
| $('pg-temp-v').textContent = parseFloat($('pg-temp').value).toFixed(2); | |
| $('pg-topp-v').textContent = parseFloat($('pg-topp').value).toFixed(2); | |
| $('test-btn').addEventListener('click', async () => { | |
| const out = $('test-out'); | |
| const base = $('base-url').value.trim().replace(/\/+$/, ''); | |
| if (!base) { out.innerHTML = '<span class="badge-bad">⚠️ set a base URL first.</span>'; return; } | |
| out.textContent = 'calling ' + base + '/models …'; | |
| try { | |
| const res = await fetch(base + '/models', { | |
| headers: $('api-key').value ? { Authorization: 'Bearer ' + $('api-key').value } : {} | |
| }); | |
| if (!res.ok) throw new Error('HTTP ' + res.status + ' ' + res.statusText); | |
| const json = await res.json(); | |
| const ids = (json.data || []).map((m) => m.id); | |
| out.innerHTML = '<span class="badge-ok">✅ ' + ids.length + ' model(s):</span> ' + | |
| ids.slice(0, 20).map((i) => '<code>' + escapeHtml(i) + '</code>').join(' ') + | |
| (ids.length > 20 ? ' …' : ''); | |
| } catch (err) { | |
| out.innerHTML = '<span class="badge-bad">❌ ' + escapeHtml(err.message) + | |
| '</span><br><span class="note">A browser-side "Failed to fetch" here usually means CORS or mixed ' + | |
| 'content (http endpoint on an https page), not that the server is down.</span>'; | |
| } | |
| }); | |
| async function streamCompletion(body, onDelta, onReasoning) { | |
| const base = $('base-url').value.trim().replace(/\/+$/, ''); | |
| const key = $('api-key').value; | |
| const res = await fetch(base + '/chat/completions', { | |
| method: 'POST', | |
| headers: Object.assign({ 'Content-Type': 'application/json' }, key ? { Authorization: 'Bearer ' + key } : {}), | |
| body: JSON.stringify(body) | |
| }); | |
| if (!res.ok) { | |
| let detail = ''; | |
| try { detail = (await res.text()).slice(0, 400); } catch (err) { /* ignore */ } | |
| const err = new Error('HTTP ' + res.status + ' ' + res.statusText + (detail ? ' — ' + detail : '')); | |
| err.status = res.status; | |
| throw err; | |
| } | |
| if (!res.body) { // non-streaming fallback | |
| const json = await res.json(); | |
| const choice = (json.choices || [])[0] || {}; | |
| const msg = choice.message || {}; | |
| if (msg.reasoning_content) onReasoning(msg.reasoning_content); | |
| if (msg.content) onDelta(msg.content); | |
| return; | |
| } | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ''; | |
| for (;;) { | |
| const chunk = await reader.read(); | |
| if (chunk.done) break; | |
| buffer += decoder.decode(chunk.value, { stream: true }); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop(); | |
| for (const line of lines) { | |
| const trimmed = line.trim(); | |
| if (!trimmed.startsWith('data:')) continue; | |
| const payload = trimmed.slice(5).trim(); | |
| if (payload === '[DONE]') continue; | |
| let json; | |
| try { json = JSON.parse(payload); } catch (err) { continue; } | |
| const delta = ((json.choices || [])[0] || {}).delta || {}; | |
| const reasoning = delta.reasoning_content || delta.reasoning || ''; | |
| if (reasoning) onReasoning(reasoning); | |
| if (delta.content) onDelta(delta.content); | |
| } | |
| } | |
| } | |
| $('send-btn').addEventListener('click', send); | |
| $('msg-in').addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); send(); } | |
| }); | |
| async function send() { | |
| if (streaming) return; | |
| const text = $('msg-in').value.trim(); | |
| if (!text) return; | |
| if (!$('base-url').value.trim() || !$('model-name').value.trim()) { | |
| chatHistory.push({ role: 'assistant', error: true, content: '⚠️ Set a base URL and model first (or pick a preset).' }); | |
| renderChat(); | |
| return; | |
| } | |
| streaming = true; | |
| $('stream-status').textContent = 'streaming…'; | |
| chatHistory.push({ role: 'user', content: text }); | |
| chatHistory.push({ role: 'assistant', content: '' }); | |
| $('msg-in').value = ''; | |
| $('reasoning-out').value = ''; | |
| renderChat(); | |
| const messages = []; | |
| const sys = $('system-in').value.trim(); | |
| if (sys) messages.push({ role: 'system', content: sys }); | |
| chatHistory.slice(0, -1).forEach((m) => { if (!m.error) messages.push({ role: m.role, content: m.content }); }); | |
| const body = { | |
| model: $('model-name').value.trim(), | |
| messages: messages, | |
| temperature: parseFloat($('pg-temp').value), | |
| top_p: parseFloat($('pg-topp').value), | |
| max_tokens: parseInt($('pg-maxtok').value, 10) || 8192, | |
| stream: true | |
| }; | |
| if ($('pg-send').checked) { | |
| body.reasoning_effort = $('pg-effort').value; | |
| body.thinking_mode = $('pg-mode').value; | |
| } | |
| let answer = ''; | |
| let reasoning = ''; | |
| const onDelta = (piece) => { answer += piece; chatHistory[chatHistory.length - 1].content = answer; renderChat(); }; | |
| const onReasoning = (piece) => { reasoning += piece; $('reasoning-out').value = reasoning; }; | |
| try { | |
| await streamCompletion(body, onDelta, onReasoning); | |
| } catch (err) { | |
| if (body.reasoning_effort && /40[04]/.test(String(err.status))) { | |
| delete body.reasoning_effort; | |
| delete body.thinking_mode; | |
| chatHistory[chatHistory.length - 1].content = '_(endpoint rejected reasoning_effort/thinking_mode — retrying without)_\n\n'; | |
| renderChat(); | |
| try { await streamCompletion(body, onDelta, onReasoning); } | |
| catch (err2) { fail(err2); } | |
| } else { | |
| fail(err); | |
| } | |
| } | |
| function fail(err) { | |
| const msg = String(err && err.message ? err.message : err); | |
| chatHistory[chatHistory.length - 1] = { | |
| role: 'assistant', error: true, | |
| content: '❌ request failed — ' + msg + '\n\nChecklist: reachable from this browser? model name correct ' + | |
| '(use Test endpoint)? CORS / mixed-content? key valid?' | |
| }; | |
| renderChat(); | |
| } | |
| if (!chatHistory[chatHistory.length - 1].content) { | |
| chatHistory[chatHistory.length - 1].content = '⚠️ the endpoint streamed no content (check the model name).'; | |
| renderChat(); | |
| } | |
| $('stream-status').textContent = ''; | |
| streaming = false; | |
| } | |
| $('clear-btn').addEventListener('click', () => { | |
| chatHistory.length = 0; | |
| $('reasoning-out').value = ''; | |
| $('stream-status').textContent = ''; | |
| renderChat(); | |
| }); | |
| // ============================================================ boot | |
| updateSize(); | |
| mountEncoder(); | |
| renderChat(); | |