xdLocutus's picture
Upload app.js with huggingface_hub
187ada9 verified
Raw History Blame Contribute Delete
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.
*/
'use strict';
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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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>&lt;|Assistant|&gt;</code>' +
(thinking === 'thinking' ? ' plus <code>&lt;think&gt;</code>.' : ' plus an immediate <code>&lt;/think&gt;</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>&lt;/think&gt;</code> and end the turn with <code>&lt;|end▁of▁sentence|&gt;</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();