Merge #784: a local model catalogue for every Mac memory size, and its weekly check

This commit is contained in:
Codeman maintainer committed 2026-11-08 11:10:13 +01:00
commit 8a54ded78b
21 files changed
+3199 -476

No files matched your search

+751
View File
@@ -0,0 +1,751 @@
/**
* @fileoverview scripts/check-local-model-catalog.mjs, the weekly check of the local model
* catalogue: which build a Mac pulls from a manifest list, the pull total, how a row's drift is
* classified, the exit code, the report text, and that the IO side only ever asks for
* manifests, child manifests and configs (never a layer). The network is never used: the IO
* tests drive a fake registry and a fake GitHub API.
*
* The fixtures under test/fixtures/local-model-catalog/ are real bodies the registry served on
* 2026-11-08 for `qwen3.6:35b-a3b`, byte for byte, so their digests are the real ones.
*/
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import {
CheckFailure,
MANIFEST_ACCEPT,
MAX_MANIFEST_BYTES,
MEDIA_TYPE_MANIFEST,
MEDIA_TYPE_MANIFEST_LIST,
SIZE_TOLERANCE_FRACTION,
boundedGet,
catalogueRunner,
classifyModel,
classifyRuntime,
compareVersions,
exitCodeFor,
formatPercent,
groupDigits,
inspectModel,
metadataForConfig,
normalizeDigest,
parseVersion,
pullTotalBytes,
renderReport,
runCheck,
selectMacChild,
sha256Digest,
splitModelId,
withinTolerance,
} from '../scripts/check-local-model-catalog.mjs';
const fixtureDir = resolve(import.meta.dirname, 'fixtures/local-model-catalog');
const fixture = (name: string): Buffer => readFileSync(resolve(fixtureDir, name));
const REAL_LIST = fixture('qwen3.6-35b-a3b.list.json');
const REAL_GGUF_MANIFEST = fixture('qwen3.6-35b-a3b.gguf-manifest.json');
const REAL_MLX_CONFIG = fixture('qwen3.6-35b-a3b.mlx-config.json');
/** The digest of the MLX child the real list names, which a Mac pulled on 2026-11-06. */
const REAL_MLX_CHILD = 'sha256:0ce5ff81f9ffdf89dd8192ca7b5d34cefd8996a875b3aaaaafaf5c4813569eba';
const REAL_GGUF_CHILD = 'sha256:a7eb95c53bcf96b4bdd008d0fab4a5dac88047d9c1a7a9ab88ed453423fbd87c';
/** The pull total a Mac reported for `qwen3.6:35b-a3b`, which the catalogue row stores. */
const MAC_PULL_TOTAL_35B = 23_105_798_956;
const PIN = {
version: '0.40.15',
url: 'https://github.com/ollama/ollama/releases/download/v0.40.15/ollama-darwin.tgz',
bytes: 185_971_671,
sha256: '607c79d5d6315ef9d896db83adde736a4c0057196329843544e79b95afb260a8',
};
const hex = (char: string): string => `sha256:${char.repeat(64)}`;
describe('the real registry bodies', () => {
it('carry the digests the registry and a Mac reported for them', () => {
expect(sha256Digest(REAL_LIST)).toBe('sha256:86b2d171ce90bbb2dcb081933565ec867ce523cdd274bd24c8cf1ef9185feb4f');
expect(sha256Digest(REAL_GGUF_MANIFEST)).toBe(REAL_GGUF_CHILD);
expect(sha256Digest(REAL_MLX_CONFIG)).toBe(
'sha256:577d7d4694896a245c8b00f62bf53a8f4720ded4eba83a8e2da30bb87583bd6b'
);
});
it('a Mac selects the mlx child of the real list, the one whose digest /api/tags reported', () => {
const list = JSON.parse(REAL_LIST.toString('utf8'));
expect(list.mediaType).toBe(MEDIA_TYPE_MANIFEST_LIST);
expect(selectMacChild(list.manifests).digest).toBe(REAL_MLX_CHILD);
});
it('the plain GGUF manifest totals its layers plus its config, 2% below what a Mac pulls', () => {
const manifest = JSON.parse(REAL_GGUF_MANIFEST.toString('utf8'));
expect(pullTotalBytes(manifest)).toBe(22_621_314_161 + 220);
expect(classifyModel(
{ id: 'qwen3.6:35b-a3b', bytes: MAC_PULL_TOTAL_35B },
{ mac: { bytes: pullTotalBytes(manifest), digest: REAL_GGUF_CHILD } },
'0.40.15'
)).toEqual([expect.objectContaining({ kind: 'size' })]);
});
});
describe('splitModelId', () => {
it('splits name and tag', () => {
expect(splitModelId('qwen3.6:35b-a3b')).toEqual({ name: 'qwen3.6', tag: '35b-a3b' });
});
it.each(['qwen3.6', ':35b', 'qwen3.6:', 'evil.example/x:y'])('refuses %s', (id) => {
expect(() => splitModelId(id)).toThrow(CheckFailure);
});
});
describe('pullTotalBytes', () => {
const manifest = (layers: unknown, config: unknown = { size: 250 }) => ({ config, layers });
it('adds every layer and the config', () => {
expect(pullTotalBytes(manifest([{ size: 1000 }, { size: 24 }]))).toBe(1274);
});
it.each([
['no layers', manifest([])],
['layers that are not an array', manifest('x')],
['a layer without a size', manifest([{ digest: 'x' }])],
['a negative size', manifest([{ size: -1 }])],
['a size given as text', manifest([{ size: '1000' }])],
['a fractional size', manifest([{ size: 1.5 }])],
['no config size', manifest([{ size: 1 }], {})],
['a total past the safe integers', manifest([{ size: Number.MAX_SAFE_INTEGER }, { size: 10 }])],
])('refuses %s', (_label, value) => {
expect(() => pullTotalBytes(value)).toThrow(CheckFailure);
});
});
describe('selectMacChild', () => {
const child = (runner: string, digest: string, mediaType?: string) => ({
...(mediaType === undefined ? { mediaType: MEDIA_TYPE_MANIFEST } : { mediaType }),
digest,
runner,
});
it('prefers mlx, then llamacpp, then ggml, whatever the list order', () => {
expect(selectMacChild([child('ggml', 'g'), child('llamacpp', 'l'), child('mlx', 'm')]).digest).toBe('m');
expect(selectMacChild([child('ggml', 'g'), child('llamacpp', 'l')]).digest).toBe('l');
expect(selectMacChild([child('ggml', 'g')]).digest).toBe('g');
});
it('takes the first child of a runner and compares runners as Ollama does', () => {
expect(selectMacChild([child(' MLX ', 'first'), child('mlx', 'second')]).digest).toBe('first');
});
it('skips a nested list and a runner a Mac does not run', () => {
expect(selectMacChild([child('mlx', 'nested', MEDIA_TYPE_MANIFEST_LIST), child('llamacpp', 'l')]).digest).toBe('l');
expect(selectMacChild([child('vllm', 'v')])).toBeUndefined();
expect(selectMacChild('not a list')).toBeUndefined();
});
it('accepts a child without a mediaType', () => {
expect(selectMacChild([{ digest: 'm', runner: 'mlx' }]).digest).toBe('m');
});
});
describe('small helpers', () => {
it('metadataForConfig mirrors Ollama: safetensors runs on mlx, gguf on ggml', () => {
expect(metadataForConfig({ model_format: 'safetensors' })).toEqual({ runner: 'mlx', format: 'safetensors' });
expect(metadataForConfig({ model_format: 'gguf' })).toEqual({ runner: 'ggml', format: 'gguf' });
expect(metadataForConfig({})).toEqual({ runner: '', format: '' });
});
it('catalogueRunner names a build in the catalogue words: mlx, or llamacpp for any GGUF build', () => {
expect(catalogueRunner('mlx')).toBe('mlx');
expect(catalogueRunner(' MLX ')).toBe('mlx');
expect(catalogueRunner('llamacpp')).toBe('llamacpp');
// A GGUF manifest that names no runner is `ggml` to Ollama, and llama-server runs it all the same.
expect(catalogueRunner('ggml')).toBe('llamacpp');
expect(catalogueRunner(metadataForConfig({ model_format: 'gguf' }).runner)).toBe('llamacpp');
for (const other of ['', 'vllm', '<img>', undefined, 3]) expect(catalogueRunner(other)).toBeUndefined();
});
it('normalizeDigest accepts the prefixed and the bare form and nothing else', () => {
const bare = '0ce5ff81f9ffdf89dd8192ca7b5d34cefd8996a875b3aaaaafaf5c4813569eba';
expect(normalizeDigest(bare)).toBe(REAL_MLX_CHILD);
expect(normalizeDigest(REAL_MLX_CHILD.toUpperCase().replace('SHA256', 'sha256'))).toBe(REAL_MLX_CHILD);
expect(normalizeDigest('sha256:abc')).toBeUndefined();
expect(normalizeDigest('sha512:' + 'a'.repeat(64))).toBeUndefined();
expect(normalizeDigest(42)).toBeUndefined();
});
it('parses and compares versions', () => {
expect(parseVersion('v0.40.15')).toEqual([0, 40, 15]);
expect(parseVersion('0.32.12')).toEqual([0, 32, 12]);
expect(parseVersion('0.40.15-rc1')).toBeUndefined();
expect(compareVersions('0.40.15', 'v0.40.15')).toBe(0);
expect(compareVersions('0.41.0', '0.40.15')).toBeGreaterThan(0);
expect(compareVersions('0.9.0', '0.40.15')).toBeLessThan(0);
expect(() => compareVersions('latest', '0.40.15')).toThrow(CheckFailure);
});
it('formats numbers without the locale', () => {
expect(groupDigits(23_105_798_956)).toBe('23,105,798,956');
expect(groupDigits(250)).toBe('250');
expect(formatPercent(0.16614)).toBe('+16.61%');
expect(formatPercent(-0.02097)).toBe('-2.10%');
});
it('the tolerance is a small fraction of the row', () => {
expect(SIZE_TOLERANCE_FRACTION).toBeLessThanOrEqual(0.01);
expect(withinTolerance(1_000_000, 1_001_000)).toBe(true);
expect(withinTolerance(1_000_000, 1_001_001)).toBe(false);
});
});
describe('classifyModel', () => {
const observed = (bytes: number, extra: Record<string, unknown> = {}) => ({
mac: { bytes, digest: REAL_MLX_CHILD, runner: 'mlx', format: 'safetensors', ...extra },
});
it('a row that holds the pull total itself is clean', () => {
expect(classifyModel({ id: 'qwen3.6:35b-a3b', bytes: MAC_PULL_TOTAL_35B }, observed(MAC_PULL_TOTAL_35B), '0.40.15')).toEqual([]);
});
it('a row sized from its layers alone is clean: the config is a few hundred bytes', () => {
expect(classifyModel({ id: 'gpt-oss:20b', bytes: 13_793_441_427 }, observed(13_793_441_566), '0.40.15')).toEqual([]);
});
it('flags the two real drifts of 2026-11-08: the Mac gets the mlx build', () => {
const nine = classifyModel({ id: 'qwen3.5:9b', bytes: 6_550_825_373 }, observed(7_639_229_431), '0.40.15');
expect(nine).toEqual([expect.objectContaining({ kind: 'size', expected: 6_550_825_373, actual: 7_639_229_431 })]);
expect(nine[0].fraction).toBeCloseTo(0.1661, 3);
const dense = classifyModel({ id: 'qwen3.8:27b', bytes: 17_741_871_939 }, observed(18_174_721_847), '0.40.15');
expect(dense[0].kind).toBe('size');
expect(dense[0].fraction).toBeCloseTo(0.0244, 3);
});
it('a tag that is gone is only that', () => {
expect(classifyModel({ id: 'x:y', bytes: 1 }, { gone: true }, '0.40.15')).toEqual([{ kind: 'gone' }]);
});
it('compares a pinned digest in either form', () => {
const bare = REAL_MLX_CHILD.slice('sha256:'.length);
expect(classifyModel({ id: 'a:b', bytes: 100, digest: bare }, observed(100), '0.40.15')).toEqual([]);
expect(classifyModel({ id: 'a:b', bytes: 100, digest: hex('1') }, observed(100), '0.40.15')).toEqual([
{ kind: 'digest', expected: hex('1'), actual: REAL_MLX_CHILD },
]);
expect(classifyModel({ id: 'a:b', bytes: 100, digest: 'garbage' }, observed(100), '0.40.15')[0]).toMatchObject({
kind: 'digest',
expected: 'not a sha256 digest',
});
});
it('holds a row that names its runner to the runner of the build a Mac pulls', () => {
// The same size, another runner: the row's memory figures no longer describe what runs.
expect(classifyModel({ id: 'a:b', bytes: 100, runner: 'llamacpp' }, observed(100), '0.40.15')).toEqual([
{ kind: 'runner', expected: 'llamacpp', actual: 'mlx' },
]);
expect(classifyModel({ id: 'a:b', bytes: 100, runner: 'mlx' }, observed(100), '0.40.15')).toEqual([]);
// A GGUF build is a llamacpp row's, whether its manifest says llamacpp or nothing (ggml).
for (const runner of ['llamacpp', 'ggml']) {
const gguf = observed(100, { runner, format: 'gguf' });
expect(classifyModel({ id: 'a:b', bytes: 100, runner: 'llamacpp' }, gguf, '0.40.15'), runner).toEqual([]);
expect(classifyModel({ id: 'a:b', bytes: 100, runner: 'mlx' }, gguf, '0.40.15'), runner).toEqual([
{ kind: 'runner', expected: 'mlx', actual: 'llamacpp' },
]);
}
// A runner Ollama would not run on a Mac is never taken for the row's.
expect(classifyModel({ id: 'a:b', bytes: 100, runner: 'mlx' }, observed(100, { runner: '' }), '0.40.15')).toEqual([
{ kind: 'runner', expected: 'mlx', actual: 'unknown' },
]);
// A catalogue without the field (before rows named their runner) is never held to one.
expect(classifyModel({ id: 'a:b', bytes: 100 }, observed(100, { runner: 'llamacpp' }), '0.40.15')).toEqual([]);
});
it('flags a build that needs a newer runtime than the pin, and only that', () => {
expect(classifyModel({ id: 'a:b', bytes: 100 }, observed(100, { requires: '0.41.0' }), '0.40.15')).toEqual([
{ kind: 'requires', requires: '0.41.0', pinned: '0.40.15' },
]);
expect(classifyModel({ id: 'a:b', bytes: 100 }, observed(100, { requires: '0.40.15' }), '0.40.15')).toEqual([]);
expect(classifyModel({ id: 'a:b', bytes: 100 }, observed(100), '0.40.15')).toEqual([]);
});
});
describe('classifyRuntime', () => {
const release = (version: string, asset?: { size: number; digest?: string }) => ({ version, publishedOn: '2026-11-03', asset });
const matching = { size: PIN.bytes, digest: `sha256:${PIN.sha256}` };
it('is clean when the pin is the newest release and its archive is unchanged', () => {
expect(classifyRuntime(PIN, { latest: release('0.40.15', matching), pinned: release('0.40.15', matching) })).toEqual([]);
});
it('a size that matches and no digest in the API is not drift', () => {
expect(
classifyRuntime(PIN, { latest: release('0.40.15'), pinned: release('0.40.15', { size: PIN.bytes }) })
).toEqual([]);
});
it('reports a newer release', () => {
expect(classifyRuntime(PIN, { latest: release('0.41.2'), pinned: release('0.40.15', matching) })).toEqual([
{ kind: 'behind', pinned: '0.40.15', latest: '0.41.2' },
]);
});
it('reports a pinned release or archive that is gone or changed', () => {
const latest = release('0.40.15');
expect(classifyRuntime(PIN, { latest, pinned: { gone: true } })).toEqual([{ kind: 'pinned-release-gone' }]);
expect(classifyRuntime(PIN, { latest, pinned: release('0.40.15') })).toEqual([{ kind: 'pinned-asset-gone' }]);
expect(
classifyRuntime(PIN, { latest, pinned: release('0.40.15', { size: PIN.bytes + 1, digest: matching.digest }) })[0].kind
).toBe('pinned-asset-changed');
expect(
classifyRuntime(PIN, { latest, pinned: release('0.40.15', { size: PIN.bytes, digest: hex('2') }) })[0].kind
).toBe('pinned-asset-changed');
});
});
// ---------- Exit code and report ----------
function baseResult(overrides: Record<string, unknown> = {}) {
const mac = { bytes: 100, digest: hex('a'), runner: 'mlx', format: 'safetensors', requires: '0.32.4' };
return {
checkedAt: '2026-11-09 06:00 UTC',
catalogue: { updated: '2026-11-06' },
pin: { version: '0.40.15' },
errors: [] as string[],
models: [{ id: 'a:b', bytes: 100, findings: [] as unknown[], observed: { tagDigest: hex('b'), variants: [mac], mac } }],
runtime: {
assetName: 'ollama-darwin.tgz',
findings: [] as unknown[],
observed: {
latest: { version: '0.40.15', publishedOn: '2026-11-03', asset: { size: PIN.bytes, digest: `sha256:${PIN.sha256}` } },
pinned: { version: '0.40.15', publishedOn: '2026-11-03', asset: { size: PIN.bytes, digest: `sha256:${PIN.sha256}` } },
},
} as Record<string, unknown>,
...overrides,
};
}
describe('exitCodeFor', () => {
it('is 0 when nothing drifted', () => {
expect(exitCodeFor(baseResult())).toBe(0);
});
it('is 1 for drift in a model or in the runtime', () => {
const result = baseResult();
result.models[0].findings = [{ kind: 'gone' }];
expect(exitCodeFor(result)).toBe(1);
const runtime = baseResult();
(runtime.runtime.findings as unknown[]).push({ kind: 'behind', pinned: '0.40.15', latest: '0.41.0' });
expect(exitCodeFor(runtime)).toBe(1);
});
it('is 2 when any part failed, even with drift elsewhere', () => {
const result = baseResult();
result.models[0].findings = [{ kind: 'gone' }];
result.models.push({ id: 'c:d', bytes: 1, findings: [], error: 'timed out' } as never);
expect(exitCodeFor(result)).toBe(2);
expect(exitCodeFor(baseResult({ runtime: { error: 'GitHub answered 403', findings: [] } }))).toBe(2);
expect(exitCodeFor(baseResult({ errors: ['the catalogue did not load'] }))).toBe(2);
});
});
describe('renderReport', () => {
it('says clean, and lists the digests to pin in full with the build a Mac pulls', () => {
const report = renderReport(baseResult());
expect(report).toMatch(/^# Local model catalogue: weekly check\n/);
expect(report).toContain('**Clean**');
expect(report).toContain(`| \`a:b\` | mlx, safetensors | \`${hex('b')}\` | \`${hex('a')}\` |`);
expect(report).toContain('as the row field `digest`');
expect(report).not.toContain('Pin first');
expect(report).toContain('## Not checked');
});
it('asks to pin a row with measured figures first', () => {
const result = baseResult();
(result.models[0] as Record<string, unknown>).figures = 'measured';
expect(renderReport(result)).toContain('Pin first: `a:b`.');
});
it('names a size drift with both figures and every build the tag offers', () => {
const gguf = { bytes: 90, digest: hex('c'), runner: 'llamacpp', format: 'gguf' };
const mac = { bytes: 7_639_229_431, digest: hex('a'), runner: 'mlx', format: 'safetensors' };
const result = baseResult({
models: [
{
id: 'qwen3.5:9b',
bytes: 6_550_825_373,
findings: classifyModel({ id: 'qwen3.5:9b', bytes: 6_550_825_373 }, { mac }, '0.40.15'),
observed: { tagDigest: hex('b'), variants: [gguf, mac], mac },
},
],
});
const report = renderReport(result);
expect(report).toContain('**Drift found**: 1 of 1 models.');
expect(report).toContain('| `qwen3.5:9b` | 6,550,825,373 | 7,639,229,431 | +16.61% | mlx, safetensors |');
expect(report).toContain('mlx, safetensors 7,639,229,431 (a Mac takes this one)');
expect(report).toContain('llamacpp, gguf 90');
});
it('names a runner change, what it means for the figures, and every build the tag offers', () => {
const gguf = { bytes: 100, digest: hex('c'), runner: 'llamacpp', format: 'gguf' };
const mac = { bytes: 100, digest: hex('a'), runner: 'mlx', format: 'safetensors' };
const result = baseResult({
models: [
{
id: 'qwen3.6:35b-a3b-mtp-q4_k_m',
bytes: 100,
findings: classifyModel({ id: 'x:y', bytes: 100, runner: 'llamacpp' }, { mac }, '0.40.15'),
observed: { tagDigest: hex('b'), variants: [gguf, mac], mac },
},
],
});
const report = renderReport(result);
expect(report).toContain('**Drift found**: 1 of 1 models.');
expect(report).toContain('| runner changed |');
expect(report).toContain(
"`qwen3.6:35b-a3b-mtp-q4_k_m`: a Mac now pulls the mlx, safetensors build, which runs on `mlx`, while the row's `runner` says `llamacpp`."
);
expect(report).toContain('recompute them for the new runner');
expect(report).toContain('mlx, safetensors 100 (a Mac takes this one)');
});
it('says the check failed and prints no verdict of clean', () => {
const result = baseResult({ runtime: { error: 'GitHub answered 403 for the newest release', findings: [] } });
const report = renderReport(result);
expect(report).toContain('**The check failed**');
expect(report).not.toContain('**Clean**');
expect(report).toContain('GitHub answered 403');
});
it('never prints a runner or format outside the known names', () => {
const mac = { bytes: 100, digest: hex('a'), runner: '<img src=x>', format: '@someone' };
const report = renderReport(baseResult({ models: [{ id: 'a:b', bytes: 100, findings: [], observed: { tagDigest: hex('b'), variants: [mac], mac } }] }));
expect(report).toContain('unknown, unknown');
expect(report).not.toContain('<img');
expect(report).not.toContain('@someone');
});
it('keeps an error on one line and without backticks', () => {
const result = baseResult({ errors: ['line one\nline `two`\u0007'] });
expect(renderReport(result)).toContain("- Check failed: line one line 'two'\n");
});
it('contains no em-dash', () => {
expect(renderReport(baseResult())).not.toContain(String.fromCharCode(0x2014));
});
});
// ---------- IO against a fake registry ----------
interface Seen {
url: string;
headers: Record<string, string>;
signal: unknown;
}
/**
* A fake registry and GitHub API. `routes` maps a URL to a body (string or Buffer) or a status
* number. Every request is recorded.
*/
function fakeFetch(routes: Record<string, string | Buffer | number | (() => Response)>) {
const seen: Seen[] = [];
const fetchFn = async (url: string, init: { headers: Record<string, string>; signal: unknown }) => {
seen.push({ url, headers: init.headers, signal: init.signal });
const route = routes[url];
if (route === undefined) return new Response('not found', { status: 404 });
if (typeof route === 'number') return new Response('', { status: route });
if (typeof route === 'function') return route();
return new Response(route, { status: 200 });
};
return { fetchFn, seen };
}
const REG = 'https://registry.ollama.ai/v2/library';
const GH = 'https://api.github.com/repos/ollama/ollama/releases';
/** A small registry: one tag that is a list (mlx + llamacpp), one plain tag. Digests computed. */
function buildRegistry() {
const json = (value: unknown): Buffer => Buffer.from(JSON.stringify(value));
const mlxConfig = json({ model_format: 'safetensors', requires: '0.32.4' });
const ggufConfig = json({ model_format: 'gguf', requires: '0.30.0' });
const layers = (sizes: number[], tag: string) =>
sizes.map((size, i) => ({ mediaType: 'application/vnd.ollama.image.tensor', digest: hex(String((i + tag.length) % 10)), size }));
const mlxChild = json({
schemaVersion: 2,
mediaType: MEDIA_TYPE_MANIFEST,
config: { digest: sha256Digest(mlxConfig), size: mlxConfig.length },
layers: layers([700, 300], 'mlx'),
});
const ggufChild = json({
schemaVersion: 2,
mediaType: MEDIA_TYPE_MANIFEST,
config: { digest: sha256Digest(ggufConfig), size: ggufConfig.length },
layers: layers([600], 'gguf'),
runner: 'llamacpp',
format: 'gguf',
});
const list = json({
schemaVersion: 2,
mediaType: MEDIA_TYPE_MANIFEST_LIST,
manifests: [
{ mediaType: MEDIA_TYPE_MANIFEST, digest: sha256Digest(mlxChild), runner: 'mlx', format: 'safetensors' },
{ mediaType: MEDIA_TYPE_MANIFEST, digest: sha256Digest(ggufChild), runner: 'llamacpp', format: 'gguf' },
],
});
const plain = json({
schemaVersion: 2,
mediaType: MEDIA_TYPE_MANIFEST,
config: { digest: sha256Digest(ggufConfig), size: ggufConfig.length },
layers: layers([500], 'plain'),
});
const release = (tag: string) =>
json({
tag_name: tag,
published_at: '2026-11-03T22:52:31Z',
assets: [{ name: 'ollama-darwin.tgz', size: PIN.bytes, digest: `sha256:${PIN.sha256}` }],
});
const routes: Record<string, string | Buffer | number | (() => Response)> = {
[`${REG}/listed/manifests/1b`]: list,
[`${REG}/listed/blobs/${sha256Digest(mlxChild)}`]: mlxChild,
[`${REG}/listed/blobs/${sha256Digest(ggufChild)}`]: ggufChild,
[`${REG}/listed/blobs/${sha256Digest(mlxConfig)}`]: mlxConfig,
[`${REG}/plain/manifests/2b`]: plain,
[`${REG}/plain/blobs/${sha256Digest(ggufConfig)}`]: ggufConfig,
[`${GH}/latest`]: release('v0.40.15'),
[`${GH}/tags/v0.40.15`]: release('v0.40.15'),
};
const layerDigests = new Set(
[mlxChild, ggufChild, plain].flatMap((body) => JSON.parse(body.toString()).layers.map((l: { digest: string }) => l.digest))
);
return {
routes,
layerDigests,
sizes: { mlx: 1000 + mlxConfig.length, gguf: 600 + ggufConfig.length, plain: 500 + ggufConfig.length },
digests: { list: sha256Digest(list), mlxChild: sha256Digest(mlxChild), plain: sha256Digest(plain) },
};
}
const catalogOf = (models: { id: string; bytes: number; digest?: string; figures?: string; runner?: string }[]) => ({
updated: '2026-11-06',
models,
});
const NOW = new Date('2026-11-09T06:00:00Z');
describe('inspectModel', () => {
it('reads a list the way a Mac does: the mlx child, its total, its requires', async () => {
const registry = buildRegistry();
const { fetchFn, seen } = fakeFetch(registry.routes);
const observed = await inspectModel(fetchFn, 'listed:1b');
expect(observed.tagDigest).toBe(registry.digests.list);
expect(observed.mac).toMatchObject({
runner: 'mlx',
format: 'safetensors',
digest: registry.digests.mlxChild,
bytes: registry.sizes.mlx,
requires: '0.32.4',
});
expect(observed.variants.map((variant: { runner: string }) => variant.runner)).toEqual(['mlx', 'llamacpp']);
expect(seen[0].headers.Accept).toBe(MANIFEST_ACCEPT);
expect(seen[0].headers.Accept).toContain(MEDIA_TYPE_MANIFEST_LIST);
expect(seen.every((request) => request.signal instanceof AbortSignal)).toBe(true);
});
it('fills a plain manifest without runner metadata from its config', async () => {
const registry = buildRegistry();
const { fetchFn } = fakeFetch(registry.routes);
const observed = await inspectModel(fetchFn, 'plain:2b');
expect(observed.mac).toMatchObject({ runner: 'ggml', format: 'gguf', digest: registry.digests.plain, bytes: registry.sizes.plain });
expect(observed.tagDigest).toBe(observed.mac.digest);
});
it('a 404 for the manifest is a tag that is gone', async () => {
const { fetchFn } = fakeFetch({});
expect(await inspectModel(fetchFn, 'missing:tag')).toEqual({ gone: true });
});
it('refuses a blob whose body does not match its digest', async () => {
const registry = buildRegistry();
const mlxUrl = `${REG}/listed/blobs/${registry.digests.mlxChild}`;
const { fetchFn } = fakeFetch({ ...registry.routes, [mlxUrl]: '{"tampered":true}' });
await expect(inspectModel(fetchFn, 'listed:1b')).rejects.toThrow(/does not match its digest/);
});
it('refuses a list with no build a Mac runs', async () => {
const list = JSON.stringify({ mediaType: MEDIA_TYPE_MANIFEST_LIST, manifests: [{ digest: hex('1'), runner: 'vllm' }] });
const { fetchFn } = fakeFetch({ [`${REG}/odd/manifests/x`]: list });
await expect(inspectModel(fetchFn, 'odd:x')).rejects.toThrow(/no build a Mac runs/);
});
it('a server error is a failed check, not a tag that is gone', async () => {
const { fetchFn } = fakeFetch({ [`${REG}/flaky/manifests/x`]: 503 });
await expect(inspectModel(fetchFn, 'flaky:x')).rejects.toThrow(/answered 503/);
});
});
describe('boundedGet', () => {
it('refuses a declared body over the cap before reading it', async () => {
const { fetchFn } = fakeFetch({
'https://x.test/big': () => new Response('{}', { status: 200, headers: { 'content-length': String(MAX_MANIFEST_BYTES + 1) } }),
});
await expect(boundedGet(fetchFn, 'https://x.test/big', { maxBytes: MAX_MANIFEST_BYTES })).rejects.toThrow(/over the cap/);
});
it('refuses a streamed body that grows past the cap', async () => {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array(600));
controller.enqueue(new Uint8Array(600));
controller.close();
},
});
const { fetchFn } = fakeFetch({ 'https://x.test/stream': () => new Response(stream, { status: 200 }) });
await expect(boundedGet(fetchFn, 'https://x.test/stream', { maxBytes: 1000 })).rejects.toThrow(/over the cap/);
});
it('turns a network error into a CheckFailure', async () => {
const fetchFn = async () => {
throw new TypeError('fetch failed', { cause: new Error('getaddrinfo ENOTFOUND registry.ollama.ai') });
};
await expect(boundedGet(fetchFn, 'https://registry.ollama.ai/v2/', { maxBytes: 10 })).rejects.toThrow(/ENOTFOUND/);
});
});
describe('runCheck', () => {
it('reports drift, and fetches only manifests, child manifests, configs and the two releases', async () => {
const registry = buildRegistry();
const { fetchFn, seen } = fakeFetch(registry.routes);
const result = await runCheck({
catalog: catalogOf([
// Sized from the llamacpp child: a Mac pulls the mlx one, so this is drift.
{ id: 'listed:1b', bytes: registry.sizes.gguf },
{ id: 'plain:2b', bytes: registry.sizes.plain },
{ id: 'missing:tag', bytes: 1000 },
]),
pin: PIN,
fetchFn,
now: NOW,
githubToken: 'token-value',
});
expect(result.models.map((model: { findings: { kind: string }[] }) => model.findings.map((f) => f.kind))).toEqual([
['size'],
[],
['gone'],
]);
expect(result.runtime.findings).toEqual([]);
expect(exitCodeFor(result)).toBe(1);
for (const request of seen) {
const blob = /\/blobs\/(sha256:[0-9a-f]{64})$/.exec(request.url);
if (blob) expect(registry.layerDigests.has(blob[1])).toBe(false);
const isGitHub = request.url.startsWith('https://api.github.com/');
expect(request.headers.Authorization === 'Bearer token-value').toBe(isGitHub);
}
expect(seen.some((request) => request.url === `${GH}/latest`)).toBe(true);
expect(seen.some((request) => request.url === `${GH}/tags/v0.40.15`)).toBe(true);
const report = renderReport(result);
expect(report).toContain('**Drift found**: 2 of 3 models.');
expect(report).toContain('`missing:tag`: the registry no longer has this tag');
});
it('holds rows that name their runner to the build a Mac pulls, a plain GGUF manifest included', async () => {
const registry = buildRegistry();
const { fetchFn } = fakeFetch(registry.routes);
const result = await runCheck({
catalog: catalogOf([
// The right size, the wrong runner: a Mac pulls the mlx child of this list.
{ id: 'listed:1b', bytes: registry.sizes.mlx, runner: 'llamacpp' },
// A manifest that names no runner is a GGUF build: a llamacpp row, clean.
{ id: 'plain:2b', bytes: registry.sizes.plain, runner: 'llamacpp' },
]),
pin: PIN,
fetchFn,
now: NOW,
});
expect(result.models.map((model: { findings: unknown[] }) => model.findings)).toEqual([
[{ kind: 'runner', expected: 'llamacpp', actual: 'mlx' }],
[],
]);
expect(exitCodeFor(result)).toBe(1);
expect(renderReport(result)).toContain('| runner changed |');
});
it('sends no Authorization header at all without a token', async () => {
const registry = buildRegistry();
const { fetchFn, seen } = fakeFetch(registry.routes);
await runCheck({ catalog: catalogOf([{ id: 'plain:2b', bytes: registry.sizes.plain }]), pin: PIN, fetchFn, now: NOW });
expect(seen.some((request) => 'Authorization' in request.headers)).toBe(false);
});
it('is clean when every row matches what a Mac pulls', async () => {
const registry = buildRegistry();
const { fetchFn } = fakeFetch(registry.routes);
const result = await runCheck({
catalog: catalogOf([
{ id: 'listed:1b', bytes: registry.sizes.mlx, digest: registry.digests.mlxChild, figures: 'measured' },
{ id: 'plain:2b', bytes: registry.sizes.plain, figures: 'measured' },
]),
pin: PIN,
fetchFn,
now: NOW,
});
expect(exitCodeFor(result)).toBe(0);
const report = renderReport(result);
expect(report).toContain('**Clean**');
// The pinned row is not offered for pinning again; the unpinned one is.
expect(report).not.toContain('| `listed:1b` | mlx');
expect(report).toContain(`| \`plain:2b\` | ggml, gguf | \`${registry.digests.plain}\``);
// Only the measured row that has no digest yet is asked for first.
expect(report).toContain('Pin first: `plain:2b`.');
});
it('a newer runtime release is drift', async () => {
const registry = buildRegistry();
registry.routes[`${GH}/latest`] = JSON.stringify({ tag_name: 'v0.41.0', published_at: '2026-11-10T00:00:00Z', assets: [] });
const { fetchFn } = fakeFetch(registry.routes);
const result = await runCheck({ catalog: catalogOf([{ id: 'plain:2b', bytes: registry.sizes.plain }]), pin: PIN, fetchFn, now: NOW });
expect(result.runtime.findings).toEqual([{ kind: 'behind', pinned: '0.40.15', latest: '0.41.0' }]);
expect(exitCodeFor(result)).toBe(1);
expect(renderReport(result)).toContain('Ollama 0.41.0 is out; the pin is 0.40.15.');
});
it('a rate-limited GitHub API fails the check (2) and keeps what the models showed', async () => {
const registry = buildRegistry();
registry.routes[`${GH}/latest`] = 403;
const { fetchFn } = fakeFetch(registry.routes);
const result = await runCheck({
catalog: catalogOf([{ id: 'listed:1b', bytes: registry.sizes.gguf }]),
pin: PIN,
fetchFn,
now: NOW,
});
expect(exitCodeFor(result)).toBe(2);
expect(result.models[0].findings[0].kind).toBe('size');
expect(renderReport(result)).toContain('**The check failed**');
});
it('a release tag that is not vX.Y.Z fails the check instead of reaching the report', async () => {
const registry = buildRegistry();
registry.routes[`${GH}/latest`] = JSON.stringify({ tag_name: 'v1.0.0 @everyone', assets: [] });
const { fetchFn } = fakeFetch(registry.routes);
const result = await runCheck({ catalog: catalogOf([]), pin: PIN, fetchFn, now: NOW });
expect(exitCodeFor(result)).toBe(2);
expect(renderReport(result)).not.toContain('@everyone');
});
it('a network failure for one row fails the whole check', async () => {
const registry = buildRegistry();
const fetchFn = async (url: string, init: { headers: Record<string, string>; signal: unknown }) => {
if (url.includes('/plain/')) throw new TypeError('fetch failed');
return fakeFetch(registry.routes).fetchFn(url, init);
};
const result = await runCheck({
catalog: catalogOf([
{ id: 'listed:1b', bytes: registry.sizes.mlx },
{ id: 'plain:2b', bytes: registry.sizes.plain },
]),
pin: PIN,
fetchFn,
now: NOW,
});
expect(result.models[1].error).toMatch(/fetch failed/);
expect(exitCodeFor(result)).toBe(2);
});
});
@@ -0,0 +1 @@
{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:99afa5bbf1e7038bda7d028e100ee76b7337c943468d52590de3282db7b52adb","size":220},"layers":[{"mediaType":"application/vnd.ollama.image.model","digest":"sha256:d372de8e934898a59e6ccfabc3368474711384d8f1fd4d22d87a3f0a45400cdc","size":21718480960,"from":"qwen3.6-source:35b-a3b-mtp-q4_K_M-20260824"},{"mediaType":"application/vnd.ollama.image.projector","digest":"sha256:a62390d25b4b4a2d8afd7cc3c90021c11935c1fdd48d7cb0723ab71cd02a598e","size":902821728,"from":"qwen3.6-source:35b-a3b-mtp-q4_K_M-20260824"},{"mediaType":"application/vnd.ollama.image.license","digest":"sha256:5f3a3c817e78f5b8a4ad2d2c458a3e4b2cce470d6c12642c4eeb12cb8a9bf51d","size":11357,"from":"qwen3.6-source:35b-a3b-mtp-q4_K_M-20260824"},{"mediaType":"application/vnd.ollama.image.params","digest":"sha256:ce44c0f3ca1f7a126724e5285be7e1afdab202186c6379515ab0c1ffd40d3ecc","size":116}],"runner":"llamacpp","format":"gguf"}
@@ -0,0 +1 @@
{"schemaVersion":2,"mediaType":"application/vnd.ollama.manifest.list.v2+json","manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"sha256:0ce5ff81f9ffdf89dd8192ca7b5d34cefd8996a875b3aaaaafaf5c4813569eba","runner":"mlx","format":"safetensors"},{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"sha256:a7eb95c53bcf96b4bdd008d0fab4a5dac88047d9c1a7a9ab88ed453423fbd87c","runner":"llamacpp","format":"gguf"}]}
@@ -0,0 +1 @@
{"architecture":"","capabilities":["completion","vision","tools","thinking"],"file_type":"nvfp4","model_families":null,"model_family":"","model_format":"safetensors","model_type":"","os":"","parser":"qwen3.5","renderer":"qwen3.5","requires":"0.32.4"}
+182
View File
@@ -0,0 +1,182 @@
/**
* @fileoverview A frozen model catalogue for the RULE tests of src/local-model/recommend.ts:
* the stock catalogue exactly as it shipped on 2026-11-06 (seven rows that all carry the
* figures measured on one model), with the `runner` field the schema gained since.
*
* Why a frozen copy and not the stock catalogue: the stock catalogue is meant to change every
* week or so (new models, new measurements). The tests of a RULE (the ranking, the disk
* filter, the tags, the `limited` line, the harness fallback) need realistic rows whose
* answers do not move with it, or every catalogue update would rewrite a hundred rule tests
* and hide the few answers that really changed. What the STOCK catalogue answers for each Mac
* is pinned separately, in test/local-model-recommend.test.ts ("every memory size") and the
* two real-Mac blocks, and that is the table a catalogue change must update on purpose.
*
* Its figures are NOT a statement about any model: the rows borrow the measured row's figures,
* which is exactly why the stock catalogue replaced them. Validated by the real schema.
*/
import type { LocalModelCatalog } from '../../src/types.js';
const MEASURED_MEMORY = {
/** 36,000 to 38,000 bytes stayed held per token of context after a prompt of 89,000 and one of 167,000 tokens. */
heldBytesPerToken: 37_000,
/** The runner held 2.5 to 3 GB beside the weights. */
overheadBytes: 3_000_000_000,
/** While a long prompt was processed the peak stood about 5 GB above what stayed held. */
peakExtraBytes: 5_000_000_000,
} as const;
/**
* The catalogue as it shipped on 2026-11-06, with the `runner` field added. Frozen: a fixture,
* not the stock catalogue.
*/
export const RULES_LOCAL_MODEL_CATALOG: LocalModelCatalog = {
schema: 1,
updated: '2026-11-06',
// The oldest macOS the model runtime supports.
minMacosMajor: 14,
// Ordered by rank. The higher rank wins among models that fit equally well and are equally
// fast. `flint:40b` is left out on purpose: its plain tag is the slow build on a Mac and it
// has an open bug with tool arguments.
models: [
{
id: 'qwen3.5:9b',
label: 'Qwen3.5 9B',
license: 'Apache-2.0',
paramsB: 9,
runner: 'mlx',
bytes: 6_550_825_373,
...MEASURED_MEMORY,
figures: 'estimated',
contexts: [65536, 32768],
rank: 1,
// 37.1 tokens per second measured on 400 GB/s.
speedPer100Gbps: 9.3,
tested: [],
},
{
id: 'qwen3.5:9b-q8_0',
label: 'Qwen3.5 9B (Q8)',
license: 'Apache-2.0',
paramsB: 9,
runner: 'llamacpp',
bytes: 10_449_217_949,
...MEASURED_MEMORY,
figures: 'estimated',
contexts: [65536, 32768],
rank: 2,
// Not measured: the 9.3 of the row above scaled by file size.
speedPer100Gbps: 5.8,
tested: [],
},
{
id: 'gpt-oss:20b',
label: 'GPT-OSS 20B',
license: 'Apache-2.0',
paramsB: 20,
runner: 'llamacpp',
bytes: 13_793_441_427,
...MEASURED_MEMORY,
figures: 'estimated',
contexts: [131072, 65536],
rank: 3,
// No speed figure: its speed is unknown on every chip.
tested: [],
},
{
id: 'qwen3.6:35b-a3b',
label: 'Qwen3.6 35B-A3B',
license: 'Apache-2.0',
paramsB: 35,
runner: 'mlx',
// What a Mac really downloads: an MLX build, 2% above the 22,621,314,161 of the manifest.
bytes: 23_105_798_956,
...MEASURED_MEMORY,
figures: 'measured',
contexts: [131072, 65536],
rank: 4,
// 54.5 tokens per second measured on 400 GB/s (3 billion parameters are active per token).
// The MLX build ran faster than this figure predicts on the one Mac we tested (67.7
// against 41.8), so on other chips the estimate is on the low side.
speedPer100Gbps: 13.6,
// Run by us with OpenCode on the MacBook Pro (Mac17,13): a four-step task with tool calls.
tested: [{ chip: 'Apple M5 Pro', memGb: 48, cli: 'opencode', date: '2026-11-06', tokensPerSecond: 67.7 }],
},
{
id: 'qwen3.8:27b',
label: 'Qwen3.8 27B',
license: 'Apache-2.0',
paramsB: 27.8,
runner: 'mlx',
bytes: 17_741_871_939,
...MEASURED_MEMORY,
figures: 'estimated',
contexts: [131072, 65536, 32768],
rank: 5,
// 11.8 tokens per second measured on 400 GB/s.
speedPer100Gbps: 2.95,
tested: [],
},
{
id: 'qwen3.8:27b-q8_0',
label: 'Qwen3.8 27B (Q8)',
license: 'Apache-2.0',
paramsB: 27.8,
runner: 'llamacpp',
bytes: 29_978_241_837,
...MEASURED_MEMORY,
figures: 'estimated',
contexts: [262144, 131072],
rank: 6,
// Not measured: the 2.95 of the row above scaled by file size.
speedPer100Gbps: 1.7,
tested: [],
},
{
id: 'qwen3.5:122b-a10b',
label: 'Qwen3.5 122B-A10B',
license: 'Apache-2.0',
paramsB: 122,
runner: 'llamacpp',
bytes: 81_370_035_877,
...MEASURED_MEMORY,
figures: 'estimated',
contexts: [131072],
rank: 7,
// No speed figure: its speed is unknown on every chip.
tested: [],
},
],
// The higher rank wins when a model can run with more than one harness.
harnesses: [
{ cli: 'opencode', minContext: 65536, rank: 3 },
// Sends a fixed prompt of about 36,000 tokens, so the first answer takes minutes.
{ cli: 'claude', minContext: 65536, rank: 2, slowFirstTurn: true },
// Gated: neither its 32768 minimum nor its handling of a `name:tag` model id is verified.
{ cli: 'pi', minContext: 32768, rank: 1, gated: true },
],
// Memory bandwidth in GB/s, most specific first. Only chips that shipped and are in the source
// table are listed. Where one name covers two bandwidths (M3 Max 300 or 400, M4 Max 410 or 546,
// M5 Max 460 or 614) the LOWER one stands, so a speed computed from it is a floor.
chips: [
{ match: 'M5 Ultra', gbps: 1228 },
{ match: 'M3 Ultra', gbps: 800 },
{ match: 'M2 Ultra', gbps: 800 },
{ match: 'M1 Ultra', gbps: 800 },
{ match: 'M5 Max', gbps: 460 },
{ match: 'M4 Max', gbps: 410 },
{ match: 'M3 Max', gbps: 300 },
{ match: 'M2 Max', gbps: 400 },
{ match: 'M1 Max', gbps: 400 },
{ match: 'M5 Pro', gbps: 307 },
{ match: 'M4 Pro', gbps: 273 },
{ match: 'M3 Pro', gbps: 150 },
{ match: 'M2 Pro', gbps: 200 },
{ match: 'M1 Pro', gbps: 200 },
{ match: 'M5', gbps: 154 },
{ match: 'M4', gbps: 120 },
{ match: 'M3', gbps: 100 },
{ match: 'M2', gbps: 100 },
{ match: 'M1', gbps: 68 },
],
};
+305 -63
View File
@@ -12,9 +12,12 @@
* hides a more specific one).
* 3. The chip lookup never guesses: a chip that no row names has no bandwidth, including one
* whose name merely contains a known name ('Apple M10', 'Apple M6 Pro').
* 4. Which figures were MEASURED on a Mac and which are carried over from the measured row:
* one row says `measured` (its size is what a Mac really downloads, and it alone has a
* tested entry), every other row says `estimated`.
* 4. Which figures were MEASURED on a Mac and which were computed: one row says `measured`
* (it alone has a tested entry), every other row says `estimated`, and every estimated
* row's three memory figures are recomputed here from its OWN model's architecture (a
* table read from each model's config.json) for its runner and the runtime's settings. Two
* rows may share figures only when they share the architecture and the runner, so a row
* can never again carry another model's measurement without saying so.
*
* Port: N/A (pure data and functions).
*/
@@ -28,6 +31,12 @@ import {
LocalModelCatalogSchema,
STOCK_LOCAL_MODEL_CATALOG,
} from '../src/config/local-model-catalog.js';
import {
RUNTIME_CTX_CHECKPOINTS,
RUNTIME_HOST_PROMPT_CACHE_MIB,
RUNTIME_KV_CACHE_TYPE,
runtimeEnv,
} from '../src/local-model/runtime.js';
import type { LocalModelCatalog, LocalModelEntry } from '../src/types.js';
/** A deep copy of the stock catalogue that a test may break. */
@@ -48,7 +57,121 @@ function withFirstModel(change: Partial<Record<keyof LocalModelEntry | 'url' | '
return { ...catalog, models: [{ ...catalog.models[0], ...change }, ...catalog.models.slice(1)] };
}
/** One MiB and one GiB, in bytes. */
const MIB = 1024 * 1024;
const GIB = 1024 * MIB;
/**
* The architecture of every model the stock catalogue lists, read from its config.json on
* Hugging Face (2026-11-08): full-attention layers, their K/V heads and head size, and the
* layers whose cache does not grow with the context (Gated DeltaNet layers with their value
* and key heads, or sliding-window layers with their window). Fine-tunes share their base's.
*/
const ARCHITECTURES = {
'Qwen3.5-9B': { attentionLayers: 8, kvHeads: 4, headDim: 256, linear: { layers: 24, vHeads: 32, kHeads: 16 } },
'Qwen3.8-27B': { attentionLayers: 16, kvHeads: 4, headDim: 256, linear: { layers: 48, vHeads: 48, kHeads: 16 } },
'Qwen3.6-35B-A3B': { attentionLayers: 10, kvHeads: 2, headDim: 256, linear: { layers: 30, vHeads: 32, kHeads: 16 } },
'Qwen3.5-122B-A10B': {
attentionLayers: 12,
kvHeads: 2,
headDim: 256,
linear: { layers: 36, vHeads: 64, kHeads: 16 },
},
'North-Mini-Code-1.0': { attentionLayers: 13, kvHeads: 4, headDim: 128, sliding: { layers: 36, window: 4096 } },
} as const;
type ArchitectureName = keyof typeof ARCHITECTURES;
type Architecture = (typeof ARCHITECTURES)[ArchitectureName];
/**
* Which architecture each stock row runs, and how many tokens the runtime drafts per step with
* its multi-token-prediction layer: 0 when the GGUF has none (`nextn_predict_layers` in the GGUF
* header, read on 2026-11-08 from the first bytes of each model layer), else the tag's own
* `draft_num_predict` (its params layer) or the runtime's default of 4 when the tag sets none.
* Prediction costs a cache for that layer and one rollback copy of the recurrent state per
* drafted token (see the catalogue header). A row the catalogue gains must be added here, or the
* test below fails: its figures cannot be checked without it.
*/
const ROW_ARCHITECTURE: Record<string, { architecture: ArchitectureName; draftTokens: number }> = {
'qwen3.5:9b-q4_k_m': { architecture: 'Qwen3.5-9B', draftTokens: 0 },
'mimo-v2.6:9b-q4_k_m': { architecture: 'Qwen3.5-9B', draftTokens: 0 },
'north-mini-code-1.0:q4_k_m': { architecture: 'North-Mini-Code-1.0', draftTokens: 0 },
// Its params layer sets `draft_num_predict` 2.
'qwen3.6:35b-a3b-mtp-q4_k_m': { architecture: 'Qwen3.6-35B-A3B', draftTokens: 2 },
'qwen3.6:35b-a3b': { architecture: 'Qwen3.6-35B-A3B', draftTokens: 0 },
// A prediction layer, and no `draft_num_predict`: the runtime's 4.
'qwen3.8:27b-q8_0': { architecture: 'Qwen3.8-27B', draftTokens: 4 },
// Its params layer sets `draft_num_predict` 4.
'qwen3.8:27b-mtp-q4_k_m': { architecture: 'Qwen3.8-27B', draftTokens: 4 },
'ornith-1.5:122b': { architecture: 'Qwen3.5-122B-A10B', draftTokens: 0 },
};
/** Bytes per cached value in each K/V cache type of llama.cpp: q8_0 keeps 32 values in 34 bytes. */
const KV_BYTES_PER_VALUE: Record<string, number> = { f16: 2, q8_0: 34 / 32 };
/**
* The cache type of llama-server's draft context, which holds the prediction layer's cache: its
* default, because the runtime sets only the main context's (`--cache-type-k`).
*/
const DRAFT_KV_CACHE_TYPE = 'f16';
/** The sliding-window cache holds the window plus one batch of 512 tokens (`n_swa + n_ubatch`). */
const SLIDING_EXTRA_CELLS = 512;
/** The head size of a Gated DeltaNet layer's keys and values, and its convolution kernel, in every model above. */
const LINEAR_HEAD_DIM = 128;
const LINEAR_CONV_KERNEL = 4;
/** The recurrent state of the linear-attention layers in bytes: the convolution tail and the delta state. */
function recurrentStateBytes(arch: Architecture, convBytes: number): number {
if (!('linear' in arch)) return 0;
const { layers, vHeads, kHeads } = arch.linear;
const convDim = vHeads * LINEAR_HEAD_DIM + 2 * kHeads * LINEAR_HEAD_DIM;
const conv = (LINEAR_CONV_KERNEL - 1) * convDim * convBytes;
const delta = vHeads * LINEAR_HEAD_DIM * LINEAR_HEAD_DIM * 4;
return layers * (conv + delta);
}
/** What a `llamacpp` row's three figures must be, from its architecture and the runtime's settings. */
function llamaCppFigures(arch: Architecture, draftTokens: number): [number, number, number] {
const predictionLayer = draftTokens > 0;
const perValue = KV_BYTES_PER_VALUE[RUNTIME_KV_CACHE_TYPE];
const perLayer = arch.kvHeads * arch.headDim * 2;
// The prediction layer's cache sits in the draft context, in the draft's cache type.
const held =
arch.attentionLayers * perLayer * perValue +
(predictionLayer ? perLayer * KV_BYTES_PER_VALUE[DRAFT_KV_CACHE_TYPE] : 0);
// llama.cpp keeps the recurrent state in f32 (both parts), the sliding window in the cache type.
const recurrent = recurrentStateBytes(arch, 4);
const sliding =
'sliding' in arch ? arch.sliding.layers * (arch.sliding.window + SLIDING_EXTRA_CELLS) * perLayer * perValue : 0;
// Live: the sliding window, and the recurrent state with one rollback copy per drafted token
// (`--spec-draft-n-max`, which `need_n_rs_seq()` turns into that many copies).
const live = sliding + recurrent * (1 + draftTokens);
// A context checkpoint is a copy of what cannot be rolled back: the recurrent state or the window.
const checkpoints = RUNTIME_CTX_CHECKPOINTS * (recurrent + sliding);
return [held, live + checkpoints + 1_000_000_000 + RUNTIME_HOST_PROMPT_CACHE_MIB * MIB, 0];
}
/** The per-token figure of the MLX runner computed from an architecture: K/V in bf16, a recurrent snapshot per 8,192 tokens. */
function mlxComputedPerToken(arch: Architecture): number {
return arch.attentionLayers * arch.kvHeads * arch.headDim * 2 * 2 + recurrentStateBytes(arch, 2) / 8192;
}
/** The figures measured for `qwen3.6:35b-a3b` on the MacBook Pro. */
const MEASURED: [number, number, number] = [37_000, 3_000_000_000, 5_000_000_000];
/** What an estimated `mlx` row's three figures must be: computed, scaled by what the measured row showed. */
function mlxFigures(arch: Architecture): [number, number, number] {
const ratio = MEASURED[0] / mlxComputedPerToken(ARCHITECTURES['Qwen3.6-35B-A3B']);
return [Math.ceil(mlxComputedPerToken(arch) * ratio), MEASURED[1] + 8 * GIB, MEASURED[2]];
}
/** A row's three memory figures. */
function figuresOf(model: LocalModelEntry): [number, number, number] {
return [model.heldBytesPerToken, model.overheadBytes, model.peakExtraBytes];
}
describe('the stock catalogue', () => {
const models = STOCK_LOCAL_MODEL_CATALOG.models;
it('passes its own schema', () => {
expect(issuesOf(STOCK_LOCAL_MODEL_CATALOG)).toEqual([]);
});
@@ -60,94 +183,114 @@ describe('the stock catalogue', () => {
expect(getLocalModelCatalog()).toBe(first);
});
it('lists the seven models of the design, without flint:40b', () => {
expect(STOCK_LOCAL_MODEL_CATALOG.models.map((m) => m.id)).toEqual([
'qwen3.5:9b',
'qwen3.5:9b-q8_0',
'gpt-oss:20b',
it('lists eight models, by rank, and none of those set aside in the header', () => {
expect(models.map((m) => m.id)).toEqual([
'qwen3.5:9b-q4_k_m',
'mimo-v2.6:9b-q4_k_m',
'north-mini-code-1.0:q4_k_m',
'qwen3.6:35b-a3b-mtp-q4_k_m',
'qwen3.6:35b-a3b',
'qwen3.8:27b',
'qwen3.8:27b-q8_0',
'qwen3.5:122b-a10b',
'qwen3.8:27b-mtp-q4_k_m',
'ornith-1.5:122b',
]);
expect(STOCK_LOCAL_MODEL_CATALOG.models.some((m) => m.id.startsWith('flint'))).toBe(false);
expect(models.map((m) => m.rank)).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
const setAside =
/^(bonsai-2|gpt-oss|flint|lfm2\.5|laguna|qwen3\.8-flash-next|qwen3\.5:122b)|^ornith-1\.5:(9b|35b|397b)/;
expect(models.filter((m) => setAside.test(m.id)).map((m) => m.id)).toEqual([]);
// The plain 9B and 27B tags are MLX builds on a Mac: their GGUF builds are listed instead.
for (const plain of ['qwen3.5:9b', 'mimo-v2.6:9b', 'qwen3.8:27b']) {
expect(models.some((m) => m.id === plain)).toBe(false);
}
});
it('carries the size a Mac really downloads where that was measured, else the registry manifest', () => {
expect(Object.fromEntries(STOCK_LOCAL_MODEL_CATALOG.models.map((m) => [m.id, m.bytes]))).toEqual({
// The sum of the layer sizes of each tag's manifest, as fetched on 2026-11-06.
'qwen3.5:9b': 6_550_825_373,
'qwen3.5:9b-q8_0': 10_449_217_949,
'gpt-oss:20b': 13_793_441_427,
// The total of the real pull on a Mac (an MLX build). Its manifest sums to 22,621,314,161.
it('names the runner a Mac uses for each tag', () => {
expect(Object.fromEntries(models.map((m) => [m.id, m.runner]))).toEqual({
'qwen3.5:9b-q4_k_m': 'llamacpp',
'mimo-v2.6:9b-q4_k_m': 'llamacpp',
'north-mini-code-1.0:q4_k_m': 'llamacpp',
'qwen3.6:35b-a3b-mtp-q4_k_m': 'llamacpp',
// Manifest lists: a Mac takes the MLX child.
'qwen3.6:35b-a3b': 'mlx',
'qwen3.8:27b-q8_0': 'llamacpp',
'qwen3.8:27b-mtp-q4_k_m': 'llamacpp',
'ornith-1.5:122b': 'mlx',
});
});
it('carries the size of the build a Mac pulls: every layer plus the config', () => {
expect(Object.fromEntries(models.map((m) => [m.id, m.bytes]))).toEqual({
// The registry's manifests as fetched on 2026-11-08, in lowercase.
'qwen3.5:9b-q4_k_m': 6_550_825_550,
'mimo-v2.6:9b-q4_k_m': 6_550_811_033,
'north-mini-code-1.0:q4_k_m': 18_593_967_008,
'qwen3.6:35b-a3b-mtp-q4_k_m': 22_621_314_381,
// The MLX child of the list, and the total of a real pull on a Mac, byte for byte.
'qwen3.6:35b-a3b': 23_105_798_956,
'qwen3.8:27b': 17_741_871_939,
'qwen3.8:27b-q8_0': 29_978_241_837,
'qwen3.5:122b-a10b': 81_370_035_877,
'qwen3.8:27b-q8_0': 29_978_242_050,
'qwen3.8:27b-mtp-q4_k_m': 17_741_872_154,
// The MLX child of the list (its GGUF child is 76,756,174,997).
'ornith-1.5:122b': 73_388_069_552,
});
});
it('says which row was measured on a Mac: one, and every other row is an estimate', () => {
expect(Object.fromEntries(STOCK_LOCAL_MODEL_CATALOG.models.map((m) => [m.id, m.figures]))).toEqual({
'qwen3.5:9b': 'estimated',
'qwen3.5:9b-q8_0': 'estimated',
'gpt-oss:20b': 'estimated',
'qwen3.6:35b-a3b': 'measured',
'qwen3.8:27b': 'estimated',
'qwen3.8:27b-q8_0': 'estimated',
'qwen3.5:122b-a10b': 'estimated',
});
});
it('gives every row the memory figures measured on the one row: 37 KB per token, 3 GB, 5 GB', () => {
for (const model of STOCK_LOCAL_MODEL_CATALOG.models) {
expect(
[model.heldBytesPerToken, model.overheadBytes, model.peakExtraBytes],
`${model.id}: an unmeasured row carries the measured row's figures, not invented ones`
).toEqual([37_000, 3_000_000_000, 5_000_000_000]);
}
// No row keeps the figure that was computed from the model's config.
expect(JSON.stringify(STOCK_LOCAL_MODEL_CATALOG)).not.toContain('kvBytesPerToken');
expect(models.filter((m) => m.figures === 'measured').map((m) => m.id)).toEqual(['qwen3.6:35b-a3b']);
expect(figuresOf(models.find((m) => m.id === 'qwen3.6:35b-a3b') as LocalModelEntry)).toEqual(MEASURED);
});
it('has one tested entry: the 35B-A3B with OpenCode on the MacBook Pro, with its measured speed', () => {
expect(
Object.fromEntries(
STOCK_LOCAL_MODEL_CATALOG.models.filter((m) => m.tested.length > 0).map((m) => [m.id, m.tested])
)
).toEqual({
expect(Object.fromEntries(models.filter((m) => m.tested.length > 0).map((m) => [m.id, m.tested]))).toEqual({
'qwen3.6:35b-a3b': [
{ chip: 'Apple M5 Pro', memGb: 48, cli: 'opencode', date: '2026-11-06', tokensPerSecond: 67.7 },
],
});
// A row may claim a test only when its figures were measured.
for (const model of STOCK_LOCAL_MODEL_CATALOG.models) {
for (const model of models) {
if (model.tested.length > 0) expect(model.figures, model.id).toBe('measured');
}
});
it('gives every model a parameter count, an id of the default registry and a license', () => {
for (const model of STOCK_LOCAL_MODEL_CATALOG.models) {
expect(model.paramsB, model.id).toBeGreaterThan(0);
expect(model.id).toMatch(LOCAL_MODEL_ID_PATTERN);
expect(model.license).toBe('Apache-2.0');
}
expect(Object.fromEntries(models.map((m) => [m.id, [m.paramsB, m.license]]))).toEqual({
'qwen3.5:9b-q4_k_m': [9, 'Apache-2.0'],
'mimo-v2.6:9b-q4_k_m': [9, 'MIT'],
'north-mini-code-1.0:q4_k_m': [30, 'Apache-2.0'],
'qwen3.6:35b-a3b-mtp-q4_k_m': [35, 'Apache-2.0'],
'qwen3.6:35b-a3b': [35, 'Apache-2.0'],
'qwen3.8:27b-q8_0': [27.8, 'Apache-2.0'],
'qwen3.8:27b-mtp-q4_k_m': [27.8, 'Apache-2.0'],
'ornith-1.5:122b': [122, 'MIT'],
});
for (const model of models) expect(model.id).toMatch(LOCAL_MODEL_ID_PATTERN);
// The two 9B rows are the ones under the 12 billion line of the `limited` verdict.
expect(STOCK_LOCAL_MODEL_CATALOG.models.filter((m) => m.paramsB < 12).map((m) => m.id)).toEqual([
'qwen3.5:9b',
'qwen3.5:9b-q8_0',
]);
expect(models.filter((m) => m.paramsB < 12).map((m) => m.id)).toEqual(['qwen3.5:9b-q4_k_m', 'mimo-v2.6:9b-q4_k_m']);
});
it('offers no model below the 65536 tokens a harness needs, nor beyond its own window', () => {
for (const model of models) {
expect(Math.min(...model.contexts), model.id).toBeGreaterThanOrEqual(65536);
expect(model.contexts[0], model.id).toBeLessThanOrEqual(262144);
}
});
it('has a speed figure only where one was measured or scaled from a measurement', () => {
expect(Object.fromEntries(STOCK_LOCAL_MODEL_CATALOG.models.map((m) => [m.id, m.speedPer100Gbps ?? null]))).toEqual({
'qwen3.5:9b': 9.3,
'qwen3.5:9b-q8_0': 5.8,
'gpt-oss:20b': null,
'qwen3.6:35b-a3b': 13.6,
'qwen3.8:27b': 2.95,
expect(Object.fromEntries(models.map((m) => [m.id, m.speedPer100Gbps ?? null]))).toEqual({
// 37.1 tokens per second on 400 GB/s, and the same for the fine-tune of the same size.
'qwen3.5:9b-q4_k_m': 9.3,
'mimo-v2.6:9b-q4_k_m': 9.3,
// The 35B-A3B's llama.cpp figure: both have 3 billion active parameters.
'north-mini-code-1.0:q4_k_m': 13.6,
// 54.5 on 400 GB/s with llama.cpp.
'qwen3.6:35b-a3b-mtp-q4_k_m': 13.6,
// 67.8 on 400 GB/s with MLX.
'qwen3.6:35b-a3b': 16.9,
// 2.95 scaled by file size.
'qwen3.8:27b-q8_0': 1.7,
'qwen3.5:122b-a10b': null,
// 11.8 on 400 GB/s with llama.cpp, without prediction.
'qwen3.8:27b-mtp-q4_k_m': 2.95,
// 16.9 x 3/10 active parameters, rounded down.
'ornith-1.5:122b': 5,
});
});
@@ -174,6 +317,92 @@ describe('the stock catalogue', () => {
});
});
describe('the memory figures of the stock rows: each row has its own', () => {
const models = STOCK_LOCAL_MODEL_CATALOG.models;
it('knows the architecture of every row, and every row of that table is in the catalogue', () => {
expect(models.map((m) => m.id).sort()).toEqual(Object.keys(ROW_ARCHITECTURE).sort());
});
it('computes the llamacpp rows for the K/V cache type and the prompt cache the runtime really sets', () => {
// The figures below assume these; runtimeEnv() is what the runtime gets.
const env = runtimeEnv({ source: {}, home: '/h', modelsDir: '/m', port: 11435, contextLength: 65536 });
expect(env.OLLAMA_KV_CACHE_TYPE).toBe(RUNTIME_KV_CACHE_TYPE);
expect(env.LLAMA_ARG_CACHE_RAM).toBe(String(RUNTIME_HOST_PROMPT_CACHE_MIB));
expect(env.LLAMA_ARG_CTX_CHECKPOINTS).toBe(String(RUNTIME_CTX_CHECKPOINTS));
expect(KV_BYTES_PER_VALUE[RUNTIME_KV_CACHE_TYPE]).toBeDefined();
// The runtime sets no cache type for the draft context, so the prediction layer's stays f16.
expect(Object.keys(env).filter((name) => /DRAFT|SPEC/.test(name))).toEqual([]);
});
it("recomputes every estimated row's figures from its own model's architecture and runner", () => {
for (const model of models.filter((m) => m.figures === 'estimated')) {
const { architecture, draftTokens } = ROW_ARCHITECTURE[model.id];
const arch = ARCHITECTURES[architecture];
const expected = model.runner === 'llamacpp' ? llamaCppFigures(arch, draftTokens) : mlxFigures(arch);
expect(figuresOf(model), `${model.id} (${architecture}, ${model.runner})`).toEqual(expected);
}
});
it('states the numbers the header and the row comments name', () => {
expect(Object.fromEntries(models.map((m) => [m.id, figuresOf(m)]))).toEqual({
// State x (1 live + 8 checkpoints) + 1.0 GB + 1 GiB.
'qwen3.5:9b-q4_k_m': [17_408, 2_547_960_320, 0],
'mimo-v2.6:9b-q4_k_m': [17_408, 2_547_960_320, 0],
// The sliding window x (1 live + 8 checkpoints) + 1.0 GB + 1 GiB.
'north-mini-code-1.0:q4_k_m': [14_144, 3_698_117_120, 0],
// State x (1 live + 2 rollback copies + 8 checkpoints) + 1.0 GB + 1 GiB; the prediction
// layer's cache in f16 per token. The 27B rows below draft 4 tokens: 4 rollback copies.
'qwen3.6:35b-a3b-mtp-q4_k_m': [12_928, 2_798_242_304, 0],
'qwen3.6:35b-a3b': [37_000, 3_000_000_000, 5_000_000_000],
'qwen3.8:27b-q8_0': [38_912, 4_113_353_216, 0],
'qwen3.8:27b-mtp-q4_k_m': [38_912, 4_113_353_216, 0],
'ornith-1.5:122b': [56_574, 11_589_934_592, 5_000_000_000],
});
// The two parts of a llamacpp overhead that are not computed from the architecture.
expect(RUNTIME_CTX_CHECKPOINTS).toBe(8);
expect(RUNTIME_HOST_PROMPT_CACHE_MIB).toBe(1024);
// The MLX calibration: 37,000 held against 28,340 computed for the measured row.
expect(mlxComputedPerToken(ARCHITECTURES['Qwen3.6-35B-A3B'])).toBe(28_340);
expect(mlxComputedPerToken(ARCHITECTURES['Qwen3.5-122B-A10B'])).toBe(43_332);
});
it('lets two rows share figures only when they share the architecture, the drafted tokens and the runner', () => {
const byFigures = new Map<string, LocalModelEntry[]>();
for (const model of models) {
const key = figuresOf(model).join('/');
byFigures.set(key, [...(byFigures.get(key) ?? []), model]);
}
const shared = [...byFigures.values()].filter((group) => group.length > 1);
// The two 9B rows (a fine-tune and its base) and the two 27B rows (two quantizations of one model).
expect(shared.map((group) => group.map((m) => m.id))).toEqual([
['qwen3.5:9b-q4_k_m', 'mimo-v2.6:9b-q4_k_m'],
['qwen3.8:27b-q8_0', 'qwen3.8:27b-mtp-q4_k_m'],
]);
for (const group of shared) {
const identity = (m: LocalModelEntry): string => JSON.stringify([ROW_ARCHITECTURE[m.id], m.runner, m.figures]);
expect(new Set(group.map(identity)).size, group.map((m) => m.id).join(', ')).toBe(1);
}
});
it("gives no estimated row the measured row's per-token or overhead figure", () => {
for (const model of models.filter((m) => m.figures === 'estimated')) {
expect(model.heldBytesPerToken, model.id).not.toBe(MEASURED[0]);
expect(model.overheadBytes, model.id).not.toBe(MEASURED[1]);
expect(figuresOf(model), model.id).not.toEqual(MEASURED);
}
});
it('gives a llamacpp row no peak (its buffers are allocated at load) and an estimated mlx row the whole prefix budget', () => {
for (const model of models) {
if (model.runner === 'llamacpp') expect(model.peakExtraBytes, model.id).toBe(0);
if (model.runner === 'mlx' && model.figures === 'estimated') {
expect(model.overheadBytes - MEASURED[1], model.id).toBe(8 * GIB);
}
}
});
});
describe('the model id rule', () => {
it('accepts name:tag in lowercase', () => {
for (const id of ['qwen3.5:9b', 'gpt-oss:20b', 'qwen3.8:27b-q8_0', 'a:b', 'x_y.z-1:2.3_4']) {
@@ -240,6 +469,19 @@ describe('the schema, field by field', () => {
expect(issuesOf(withFirstModel({ peakExtraBytes: 1.5 }))).not.toEqual([]);
});
it('requires the runner on every model row, and knows two', () => {
const row: Record<string, unknown> = { ...copy().models[0] };
delete row.runner;
const issues = issuesOf({ ...copy(), models: [row] });
expect(issues).toHaveLength(1);
expect(issues[0]).toContain('models.0.runner');
expect(issuesOf(withFirstModel({ runner: 'mlx' }))).toEqual([]);
expect(issuesOf(withFirstModel({ runner: 'llamacpp' }))).toEqual([]);
for (const runner of ['ggml', 'MLX', 'llama.cpp', '']) {
expect(issuesOf(withFirstModel({ runner })), runner).not.toEqual([]);
}
});
it('requires paramsB and tested on every model row', () => {
for (const field of ['paramsB', 'tested'] as const) {
const row: Record<string, unknown> = { ...copy().models[0] };
+11 -9
View File
@@ -51,7 +51,7 @@ describe('modelFitsBudget', () => {
const catalog = getLocalModelCatalog();
it('agrees with the recommendation on every model and context of the catalogue', () => {
for (const gb of [8, 16, 24, 32, 48, 64, 128]) {
for (const gb of [8, 16, 18, 24, 32, 36, 48, 64, 96, 128, 192, 256, 512]) {
const hw = mac(gb);
const offered = recommend(hw, catalog, harnessFacts());
const rows = [offered.pick, ...offered.alternatives].filter((row) => row !== undefined);
@@ -85,7 +85,7 @@ describe('modelFitsBudget', () => {
});
it('rules out a model that is too large, and one the catalogue does not have', () => {
expect(modelFitsBudget(mac(48), catalog, 'qwen3.5:122b-a10b', 131072)).toBe(false);
expect(modelFitsBudget(mac(48), catalog, 'ornith-1.5:122b', 131072)).toBe(false);
expect(modelFitsBudget(mac(48), catalog, 'no-such:1b', 65536)).toBe(false);
expect(modelFitsBudget(mac(48), catalog, 'qwen3.6:35b-a3b', 131072)).toBe(true);
});
@@ -96,14 +96,16 @@ describe('modelFitsBudget', () => {
expect(modelFitsBudget(mac(48), catalog, id, 131072, { measuredTotalBytes: 40_157_944_217 })).toBe(true);
// A runtime that reports less than the estimate rules out what the estimate allowed.
expect(modelFitsBudget(mac(48), catalog, id, 131072, { measuredTotalBytes: 33 * 1024 ** 3 })).toBe(false);
// And one that reports more admits what the estimate ruled out: the 27B on a 36 GB Mac.
expect(modelFitsBudget(mac(36), catalog, 'qwen3.8:27b', 65536)).toBe(false);
expect(modelFitsBudget(mac(36), catalog, 'qwen3.8:27b', 65536, { measuredTotalBytes: 27 * 1024 ** 3 })).toBe(true);
// And one that reports more admits what the estimate ruled out: on a 36 GB Mac the GGUF
// 35B-A3B at its full window needs 28.8 GB, 1.032 of the 27.9 GB estimate and 0.958 of 28 GiB.
const moeGguf = 'qwen3.6:35b-a3b-mtp-q4_k_m';
expect(modelFitsBudget(mac(36), catalog, moeGguf, 262144)).toBe(false);
expect(modelFitsBudget(mac(36), catalog, moeGguf, 262144, { measuredTotalBytes: 28 * 1024 ** 3 })).toBe(true);
// The reserve still bounds it: 100 GiB "measured" on a 48 GB Mac is 40 GiB (42.9 GB) of
// budget, so the Q8 27B with its 42.8 GB stays out (0.997) although 100 GiB would hold it.
// budget, so the 122B with its 97.4 GB stays out although 100 GiB would hold it.
const huge = { measuredTotalBytes: 100 * 1024 ** 3 };
expect(modelFitsBudget(mac(48), catalog, 'qwen3.8:27b-q8_0', 131072, huge)).toBe(false);
expect(modelFitsBudget(mac(128), catalog, 'qwen3.8:27b-q8_0', 131072, huge)).toBe(true);
expect(modelFitsBudget(mac(48), catalog, 'ornith-1.5:122b', 131072, huge)).toBe(false);
expect(modelFitsBudget(mac(128), catalog, 'ornith-1.5:122b', 131072, huge)).toBe(true);
});
it('agrees with the recommendation under a measured budget too', () => {
@@ -130,7 +132,7 @@ describe('modelFitsBudget', () => {
it('is false for a context that is no token count', () => {
for (const context of [0, -1, Number.NaN, Number.POSITIVE_INFINITY]) {
expect(modelFitsBudget(mac(128), catalog, 'qwen3.5:9b', context), String(context)).toBe(false);
expect(modelFitsBudget(mac(128), catalog, 'qwen3.5:9b-q4_k_m', context), String(context)).toBe(false);
}
});
});
+10
View File
@@ -29,6 +29,7 @@ import {
} from '../src/local-model/harness-facts.js';
import { stockInstallCommand } from '../src/cli-install-runner.js';
import { getCli, registryFilePath, reloadCliRegistry } from '../src/config/cli-registry/registry.js';
import { getLocalModelCatalog } from '../src/config/local-model-catalog.js';
import type { CliEntry } from '../src/config/cli-registry/types.js';
import type { LocalModelCatalog } from '../src/types.js';
@@ -106,6 +107,15 @@ describe('cliAcceptsCustomModelId (the launch path’s model id test, on the sto
}
});
it('accepts every model id of the stock catalogue for every harness the catalogue offers', () => {
const catalog = getLocalModelCatalog();
for (const harness of catalog.harnesses.filter((row) => !row.gated)) {
for (const model of catalog.models) {
expect(cliAcceptsCustomModelId(stockEntry(harness.cli), model.id), `${harness.cli} ${model.id}`).toBe(true);
}
}
});
it('refuses a colon for a CLI whose `--model` pattern has none, and accepts the same CLI without it', () => {
const omp = stockEntry('omp');
expect(cliAcceptsCustomModelId(omp, 'qwen3.5:9b')).toBe(false);
File diff suppressed because it is too large. Load diff
+37 -2
View File
@@ -38,8 +38,11 @@ import {
parseInferenceComputeTotal,
removeLocalModelFiles,
removePartialModelBlobs,
RUNTIME_CTX_CHECKPOINTS,
RUNTIME_DOWNLOAD_RETRIES,
RUNTIME_HOST_PROMPT_CACHE_MIB,
RUNTIME_KEEP_ALIVE,
RUNTIME_KV_CACHE_TYPE,
RUNTIME_MAX_RESTARTS,
RUNTIME_PORT_FIRST,
RUNTIME_PORT_SPAN,
@@ -260,6 +263,7 @@ describe('runtimeEnv', () => {
OLLAMA_REMOTES: 'evil.example',
OLLAMA_AUTH: '1',
LLAMA_ARG_CACHE_RAM: '0',
LLAMA_ARG_CTX_CHECKPOINTS: '64',
// Other secrets a server environment carries.
ANTHROPIC_API_KEY: 'sk-ant-secret',
OPENAI_API_KEY: 'sk-secret',
@@ -273,7 +277,7 @@ describe('runtimeEnv', () => {
DYLD_INSERT_LIBRARIES: '/tmp/evil.dylib',
};
it('holds exactly the allowlist: what is copied, the private HOME, and seven settings', () => {
it('holds exactly the allowlist: what is copied, the private HOME, and eleven settings', () => {
expect(runtimeEnv(input(SERVER_ENV))).toEqual({
PATH: '/usr/local/bin:/usr/bin:/bin',
TMPDIR: '/var/folders/xy/T/',
@@ -292,9 +296,33 @@ describe('runtimeEnv', () => {
OLLAMA_KEEP_ALIVE: '10m',
OLLAMA_NUM_PARALLEL: '1',
OLLAMA_MAX_LOADED_MODELS: '1',
// The memory settings the catalogue's llamacpp rows are computed for.
OLLAMA_KV_CACHE_TYPE: 'q8_0',
OLLAMA_FLASH_ATTENTION: '1',
LLAMA_ARG_CACHE_RAM: '1024',
LLAMA_ARG_CTX_CHECKPOINTS: '8',
});
});
it('sets the llama.cpp memory settings from the exported constants, never from the server', () => {
expect(RUNTIME_KV_CACHE_TYPE).toBe('q8_0');
expect(RUNTIME_HOST_PROMPT_CACHE_MIB).toBe(1024);
expect(RUNTIME_CTX_CHECKPOINTS).toBe(8);
const env = runtimeEnv(input(SERVER_ENV));
expect(env.OLLAMA_KV_CACHE_TYPE).toBe(RUNTIME_KV_CACHE_TYPE);
expect(env.LLAMA_ARG_CACHE_RAM).toBe(String(RUNTIME_HOST_PROMPT_CACHE_MIB));
expect(env.LLAMA_ARG_CTX_CHECKPOINTS).toBe(String(RUNTIME_CTX_CHECKPOINTS));
// The server's own `LLAMA_ARG_CACHE_RAM=0` and `LLAMA_ARG_CTX_CHECKPOINTS=64`, and a user's
// q4_0 cache, lose against ours.
const env2 = runtimeEnv(input({ ...SERVER_ENV, OLLAMA_KV_CACHE_TYPE: 'q4_0', OLLAMA_FLASH_ATTENTION: '0' }));
expect([
env2.OLLAMA_KV_CACHE_TYPE,
env2.OLLAMA_FLASH_ATTENTION,
env2.LLAMA_ARG_CACHE_RAM,
env2.LLAMA_ARG_CTX_CHECKPOINTS,
]).toEqual(['q8_0', '1', '1024', '8']);
});
it('lets no CODEMAN_ variable through, the password least of all', () => {
const env = runtimeEnv(input(SERVER_ENV));
expect(Object.keys(env).filter((name) => name.startsWith('CODEMAN_'))).toEqual([]);
@@ -309,8 +337,10 @@ describe('runtimeEnv', () => {
.sort()
).toEqual([
'OLLAMA_CONTEXT_LENGTH',
'OLLAMA_FLASH_ATTENTION',
'OLLAMA_HOST',
'OLLAMA_KEEP_ALIVE',
'OLLAMA_KV_CACHE_TYPE',
'OLLAMA_MAX_LOADED_MODELS',
'OLLAMA_MODELS',
'OLLAMA_NO_CLOUD',
@@ -329,7 +359,8 @@ describe('runtimeEnv', () => {
]) {
expect(env, name).not.toHaveProperty(name);
}
expect(env).not.toHaveProperty('LLAMA_ARG_CACHE_RAM');
// The server's `LLAMA_ARG_CACHE_RAM=0` is not copied: the value is ours.
expect(env.LLAMA_ARG_CACHE_RAM).toBe('1024');
});
it('carries no secret and no value of the server that is not on the list', () => {
@@ -365,9 +396,13 @@ describe('runtimeEnv', () => {
it('leaves out what the server does not have, and sets the rest all the same', () => {
expect(Object.keys(runtimeEnv(input({}))).sort()).toEqual([
'HOME',
'LLAMA_ARG_CACHE_RAM',
'LLAMA_ARG_CTX_CHECKPOINTS',
'OLLAMA_CONTEXT_LENGTH',
'OLLAMA_FLASH_ATTENTION',
'OLLAMA_HOST',
'OLLAMA_KEEP_ALIVE',
'OLLAMA_KV_CACHE_TYPE',
'OLLAMA_MAX_LOADED_MODELS',
'OLLAMA_MODELS',
'OLLAMA_NO_CLOUD',
+4
View File
@@ -206,6 +206,10 @@ describe('start', () => {
OLLAMA_KEEP_ALIVE: '10m',
OLLAMA_NUM_PARALLEL: '1',
OLLAMA_MAX_LOADED_MODELS: '1',
OLLAMA_KV_CACHE_TYPE: 'q8_0',
OLLAMA_FLASH_ATTENTION: '1',
LLAMA_ARG_CACHE_RAM: '1024',
LLAMA_ARG_CTX_CHECKPOINTS: '8',
});
});