File size: 5,023 Bytes
9384fec
 
 
 
 
 
 
 
 
 
3c367ec
9384fec
 
3c367ec
 
 
 
 
 
 
9384fec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3c367ec
 
9384fec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/**

 * 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();