/** * The page: one repo id in, one serving decision out. * * All rules live in parser_advice.js, which the parity gate holds to the * Python implementation. Nothing here decides anything — it fetches, * calls, and renders, so a rule can never be quietly different in the UI * from the one that was tested. */ import { ALL_MARKERS, advise, fetchTemplate } from './parser_advice.js'; import { progress } from './progress.js'; import { ensureNextStepStyles, nextSteps, repoFromUrl } from './tools.js'; const $ = (id) => document.getElementById(id); // A link from another tool carries the model over; arriving at an empty // form is what makes five tools feel like five tools instead of one. ensureNextStepStyles(); const carried = repoFromUrl(); if (carried) { $('repo').value = carried; } const out = $('out'); const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); function renderRules() { const seen = new Map(); for (const [parser, marks] of Object.entries(ALL_MARKERS)) { const key = marks.join(' '); if (!seen.has(key)) seen.set(key, []); seen.get(key).push(parser); } $('rules').innerHTML = [...seen.entries()].map(([marks, parsers]) => `${parsers.map(esc).join(', ')}` + `${marks.split(' ').map(esc).join(' ')}` ).join(''); } function card(cls, title, body) { return `

${title}

${body}
`; } function render(repo, source, result) { const meta = `
repo ${esc(repo)} template from ${esc(source)}
`; if (result.verdict === 'unknown') { return meta + card('check', 'No chat template published', `

Nothing here can be decided from metadata. The repo publishes neither chat_template.jinja nor a chat_template in tokenizer_config.json, so there is no assistant turn to read. Serving it will still work — the decision just has to come from somewhere else.

`); } if (result.verdict === 'none') { return meta + card('clear', 'Serve it without a reasoning parser', `
vllm serve ${esc(repo)}

The template carries no marker any parser closes on. Adding --reasoning-parser here is the silent failure: the parser would claim the whole answer, the request would finish with stop, and content would come back empty.

`); } const parsers = Object.keys(result.candidates).sort(); // Parsers sharing one marker set are interchangeable for this model. // Naming the family one first is a convenience, not a rule the data // supports, so it is offered as a preference rather than a verdict. const preferred = parsers.find((p) => repo.toLowerCase().includes(p.split('_')[0])) || parsers[0]; const marks = result.candidates[preferred]; const others = parsers.filter((p) => p !== preferred); const alt = others.length ? `

Interchangeable for this model, because they close on the same marker: ${others.map(esc).join(', ')}. Pick the one named for the model's family if there is one.

` : ''; return meta + card('check', 'This model thinks — it needs a parser', `
vllm serve ${esc(repo)} --reasoning-parser ${esc(preferred)}

Its template emits ${marks.map((m) => `${esc(m)}`).join(' and ')}, which ${esc(preferred)} closes on.

${alt}

Without the flag that section reaches the user as the answer.

`); } async function run(repo) { const go = $('go'); go.disabled = true; // Two small files at most, so this stays indeterminate — there is // nothing to count. It is here for the same reason as on the other // two pages: a page that shows nothing while it works reads as a page // that does not work, and the wait is not always short. const pg = progress(out).start(`reading the chat template of ${repo}`); try { const { template, source } = await fetchTemplate(repo); pg.done(); out.innerHTML = render(repo, source, advise(template)) + nextSteps('parser', repo); } catch (e) { out.innerHTML = card('problem', 'Could not read the repo', `

${esc(e.message || e)}

Gated and private repos cannot be read from the browser.

`); } finally { go.disabled = false; } } $('form').addEventListener('submit', (e) => { e.preventDefault(); const v = $('repo').value.trim(); if (v) run(v); }); document.querySelectorAll('[data-ex]').forEach((b) => b.addEventListener('click', () => { $('repo').value = b.dataset.ex; run(b.dataset.ex); })); renderRules();