Spaces:
Running
Running
Download app.js from aleada/reasoning-parser-advisor: direct link, hf CLI and curl.
- Browser
- Download file 5.02 kB
-
https://huggingface.co/spaces/aleada/reasoning-parser-advisor/resolve/main/app.js
- Command line
-
hf download hf://spaces/aleada/reasoning-parser-advisor/app.js
-
curl -L -o app.js https://huggingface.co/spaces/aleada/reasoning-parser-advisor/resolve/main/app.js
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) => | |
| ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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(); | |