Ctrl K
ComunidadGuías…

__tests__/serving-template.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { describe, it, expect } from 'vitest';
const fs = require('fs');
const path = require('path');

/*
 * A GGUF handed over without its template is served with a passthrough one, and the model answers
 * prose while the exam reads a fine number. That fault cost every export ever made, so the rule is
 * simple: the template travels with the model, and it is the template of THAT model's family.
 */
describe('the model is served the way it was trained', () => {
  const src = fs.readFileSync(path.join(__dirname, '..', 'src', 'server.js'), 'utf8');

  /* templateFor is a plain function in server.js; lift it out rather than boot the whole app. */
  const templateFor = (() => {
    const at = src.indexOf('function templateFor(');
    const end = src.indexOf('\n}', at) + 2;
    // eslint-disable-next-line no-new-func
    return new Function(`${src.slice(at, end)}; return templateFor;`)();
  })();

  it('a Qwen model is served the ChatML turns it was trained on', () => {
    const t = templateFor('Qwen/Qwen2.5-0.5B-Instruct');
    expect(t.family).toBe('chatml');
    expect(t.template).toContain('<|im_start|>system');
    expect(t.template).toContain('{{ .Prompt }}<|im_end|>');
    expect(t.template).toContain('<|im_start|>assistant');
    expect(t.stop).toEqual(['<|im_end|>', '<|im_start|>']);
    expect(t.marker).toBe('<|im_start|>');
  });

  /*
   * THIS TEST USED TO SAY "nothing about serving changes" FOR QWEN3, AND IT WAS WRONG.
   *
   * Qwen3 uses ChatML markers, so the family looked settled. What it also does is reason before
   * it answers, unless the template pre-fills an empty reasoning block - which is the only thing
   * `enable_thinking=False` does, and the exam passes that flag on every prompt it renders. The
   * first Qwen3 adapter exported on plain ChatML answered the opening move of a real job with
   * 1,406 characters of reasoning, ran out of the 320-token budget mid-thought, and parsed to
   * nothing, while its exam read 35.31%. With the block: 86 characters of correct JSON, six
   * times faster.
   */
  it('Qwen3 is served with the empty reasoning block, so it answers instead of reasoning', () => {
    const t = templateFor('Qwen/Qwen3-0.6B');
    expect(t.family).toBe('qwen3');
    expect(t.marker).toBe('<|im_start|>');
    expect(t.stop).toEqual(['<|im_end|>', '<|im_start|>']);
    const after = t.template.slice(t.template.indexOf('<|im_start|>assistant'));
    expect(after).toContain('<think>');
    expect(after.indexOf('</think>')).toBeGreaterThan(after.indexOf('<think>'));
    expect(after.indexOf('{{ .Response }}')).toBeGreaterThan(after.indexOf('</think>'));
    /* Nothing between the markers: an EMPTY block, not a prompt to reason inside. */
    const open = t.template.indexOf('<think>');
    const close = t.template.indexOf('</think>');
    expect(t.template.slice(open + 7, close).trim()).toBe('');
  });

  it('every Qwen3 size gets it, and Qwen2.5 gets none of it', () => {
    expect(templateFor('Qwen/Qwen3-1.7B').family).toBe('qwen3');
    expect(templateFor('Qwen/Qwen2.5-0.5B-Instruct').family).toBe('chatml');
    expect(templateFor('Qwen/Qwen2.5-0.5B-Instruct').template).not.toContain('<think>');
  });

  it('a Gemma model is served Gemma turns, never ChatML', () => {
    const t = templateFor('google/gemma-4-E4B-it');
    expect(t.family).toBe('gemma');
    expect(t.template).toContain('<start_of_turn>user');
    expect(t.template).toContain('<end_of_turn>');
    expect(t.template).toContain('<start_of_turn>model');
    /* No system turn: Gemma has none, and the system text rides the first user turn. */
    expect(t.template).not.toContain('<|im_start|>');
    expect(t.stop).toEqual(['<end_of_turn>', '<start_of_turn>']);
  });

  it('an unknown model falls back to the family every export so far has been', () => {
    expect(templateFor('').family).toBe('chatml');
    expect(templateFor('someone/unheard-of-7B').family).toBe('chatml');
  });

  it('the create call sends whichever template that chose, with its own stop tokens', () => {
    const call = src.slice(src.indexOf('async function createServedModel'), src.indexOf('async function createServedModel') + 4000);
    expect(call).toMatch(/const shape = templateFor\(/);
    expect(call).toMatch(/template:\s*shape\.template/);
    expect(call).toMatch(/stop:\s*shape\.stop/);
  });

  it('and the server is asked whether it really holds that family s template', () => {
    expect(src).toContain('without its ${shape.family} chat template');
    expect(src).toContain('/api/show');
    expect(src).toMatch(/includes\(shape\.marker\)/);
  });

  it('the student is given room to finish a summary', () => {
    const student = fs.readFileSync(path.join(__dirname, '..', 'src', 'student.js'), 'utf8');
    const m = /num_predict:\s*(\d+)/.exec(student);
    expect(m).toBeTruthy();
    expect(Number(m[1])).toBeGreaterThanOrEqual(300);
  });
});