aleada's picture
reasoning-parser advisor
3c367ec verified
Raw History Blame Contribute Delete
5.02 kB
/**
* 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) =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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]) =>
`<tr><td><code>${parsers.map(esc).join('</code>, <code>')}</code></td>` +
`<td><code>${marks.split(' ').map(esc).join('</code> <code>')}</code></td></tr>`
).join('');
}
function card(cls, title, body) {
return `<div class="card ${cls}"><h3>${title}</h3>${body}</div>`;
}
function render(repo, source, result) {
const meta = `<div class="meta">
<span>repo <code>${esc(repo)}</code></span>
<span>template from <code>${esc(source)}</code></span>
</div>`;
if (result.verdict === 'unknown') {
return meta + card('check', 'No chat template published',
`<p>Nothing here can be decided from metadata. The repo publishes neither
<code>chat_template.jinja</code> nor a <code>chat_template</code> in
<code>tokenizer_config.json</code>, so there is no assistant turn to read.
Serving it will still work — the decision just has to come from somewhere
else.</p>`);
}
if (result.verdict === 'none') {
return meta + card('clear', 'Serve it without a reasoning parser',
`<div class="flag">vllm serve ${esc(repo)}</div>
<p>The template carries no marker any parser closes on. Adding
<code>--reasoning-parser</code> here is the silent failure: the parser
would claim the whole answer, the request would finish with
<code>stop</code>, and <code>content</code> would come back empty.</p>`);
}
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
? `<p>Interchangeable for this model, because they close on the same marker:
<code>${others.map(esc).join('</code>, <code>')}</code>. Pick the one named
for the model's family if there is one.</p>`
: '';
return meta + card('check', 'This model thinks — it needs a parser',
`<div class="flag">vllm serve ${esc(repo)} --reasoning-parser ${esc(preferred)}</div>
<p>Its template emits ${marks.map((m) => `<code>${esc(m)}</code>`).join(' and ')},
which <code>${esc(preferred)}</code> closes on.</p>
${alt}
<p>Without the flag that section reaches the user as the answer.</p>`);
}
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',
`<p>${esc(e.message || e)}</p>
<p>Gated and private repos cannot be read from the browser.</p>`);
} 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();