mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
The "Run a local model" chooser showed the pick and four more models. It now lists every catalogue model that fits the Mac, and the install accepts every listed row. - MAX_ALTERNATIVES goes from 4 to 11, above what the catalogue holds. It stays a bound: test/local-model-catalog.test.ts fails for a catalogue that outgrows it, so nothing drops off the list unseen. - New row `muse-glimmer:30b-q4_k_m` (Meta, Apache-2.0): SWE-bench Verified 76.0, SWE-bench Pro 51.2, Terminal-Bench 2.1 51.7 on Meta's card. Its K/V cache is the smallest of the 27B class, so it is the first model of that class a 32 GB Mac holds: 21.2 GB at 65,536 tokens, 0.970 of the estimated budget. Figures computed from its config.json and its GGUF header by the llamacpp rule (13 full-attention layers, 39 sliding-window layers of 2,048 tokens, no prediction layer); the sliding-window cache rule was read again in llama.cpp b12111 (src/llama-kv-cache-iswa.cpp). Never run on a Mac: `estimated`. - What changes for a Mac: 32 GB is no longer `limited`. On an M1 Max or M2 Max the pick is Muse Glimmer; on a base or Pro chip the dense model is slow and the pick stays MiMo V2.6 9B, now with the small-pick note. Every other pick is unchanged. - The "set aside" list was read again at its sources on 2026-11-10 and gained ripple-preview:65b (every build requires runtime 0.40.16, one above the pin), Ornith 1.0 (runtime issue 16992), nemotron-3.5-super:120b, qwen3.5:4b, qwen3-coder-next and the MLX and DFlash tags of Muse Glimmer. - The header and the guide now say that runtime issue 16992 has one diagnosed cause in the tool parser every Qwen-family row shares (a tool argument that quotes a whole tool call is cut off; the fix is runtime PR 17021, open). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
167 lines
8.0 KiB
TypeScript
167 lines
8.0 KiB
TypeScript
/**
|
|
* @fileoverview Tests for `modelFitsBudget()` of src/local-model/recommend.ts: the judgement
|
|
* the install job makes before it downloads a model.
|
|
*
|
|
* Pinned: it agrees with what `recommend()` offers (a model that is the pick or listed fits,
|
|
* a model that is in neither and is not left out for its rank does not), it does not depend
|
|
* on the ranking, a model the catalogue does not have never fits, and it judges against the
|
|
* budget the runtime measured when the caller passes one (the whole point of asking again
|
|
* after the runtime started).
|
|
*
|
|
* Port: N/A (pure functions).
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import { getLocalModelCatalog, LocalModelCatalogSchema } from '../src/config/local-model-catalog.js';
|
|
import { formatGb, memClassGb } from '../src/local-model/format.js';
|
|
import { MAX_ALTERNATIVES, modelFitsBudget, recommend } from '../src/local-model/recommend.js';
|
|
import type { HardwareFacts, HarnessFact } from '../src/types.js';
|
|
|
|
/** A Mac with Apple silicon and `gb` GB of memory as sold. */
|
|
function mac(gb: number, chip = 'Apple M5 Pro'): HardwareFacts {
|
|
const memBytes = gb * 1024 ** 3;
|
|
return {
|
|
platform: 'darwin',
|
|
arch: 'arm64',
|
|
arm64: true,
|
|
translated: false,
|
|
memBytes,
|
|
memGb: formatGb(memBytes),
|
|
memClassGb: memClassGb(memBytes),
|
|
chip,
|
|
macosMajor: 27,
|
|
macosVersion: '27.2',
|
|
wiredLimitMb: 0,
|
|
source: 'sysctl',
|
|
};
|
|
}
|
|
|
|
/** Every harness of the catalogue, installed and accepting every model id. */
|
|
function harnessFacts(): HarnessFact[] {
|
|
return getLocalModelCatalog().harnesses.map((row) => ({
|
|
cli: row.cli,
|
|
label: row.cli,
|
|
installed: true,
|
|
installable: false,
|
|
acceptsModelId: () => true,
|
|
}));
|
|
}
|
|
|
|
describe('modelFitsBudget', () => {
|
|
const catalog = getLocalModelCatalog();
|
|
|
|
it('agrees with the recommendation on every model and context of the catalogue', () => {
|
|
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);
|
|
// Whatever the recommendation offers fits at the context it offers it at.
|
|
for (const row of rows) {
|
|
expect(modelFitsBudget(hw, catalog, row.modelId, row.context), `${gb} GB ${row.modelId}`).toBe(true);
|
|
}
|
|
// A Mac that gets no recommendation for lack of memory has no model that fits at a
|
|
// context a harness works at.
|
|
if (offered.verdict === 'none') {
|
|
const smallestHarnessContext = Math.min(...catalog.harnesses.filter((h) => !h.gated).map((h) => h.minContext));
|
|
for (const model of catalog.models) {
|
|
for (const context of model.contexts.filter((ctx) => ctx >= smallestHarnessContext)) {
|
|
expect(modelFitsBudget(hw, catalog, model.id, context), `${gb} GB ${model.id}`).toBe(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it('does not depend on what the recommendation lists: a model left out for disk space still fits', () => {
|
|
// The recommendation lists every model that fits the memory, unless its download would
|
|
// leave under 10 GiB on the disk. With 40 GB free on a 128 GB Mac that leaves out the two
|
|
// largest downloads, which fit the memory all the same.
|
|
const hw = { ...mac(128, 'Apple M5 Max'), freeDiskBytes: 40_000_000_000 };
|
|
const offered = recommend(hw, catalog, harnessFacts());
|
|
const listed = new Set([offered.pick, ...offered.alternatives].map((row) => row?.modelId));
|
|
const unlisted = catalog.models.filter((model) => !listed.has(model.id));
|
|
expect(unlisted.map((model) => model.id)).toEqual(['qwen3.8:27b-q8_0', 'ornith-1.5:122b']);
|
|
for (const model of unlisted) {
|
|
expect(modelFitsBudget(hw, catalog, model.id, model.contexts.at(-1) as number), model.id).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('does not depend on the ranking: a model cut off by the length of the list still fits', () => {
|
|
// A catalogue longer than the list: the stock rows plus six more copies of its smallest.
|
|
const smallest = catalog.models[0];
|
|
const long = LocalModelCatalogSchema.parse({
|
|
...structuredClone(catalog),
|
|
models: [
|
|
...structuredClone(catalog.models),
|
|
...Array.from({ length: 6 }, (_, index) => ({
|
|
...structuredClone(smallest),
|
|
id: `extra-${index + 1}:9b`,
|
|
rank: -1 - index,
|
|
})),
|
|
],
|
|
});
|
|
expect(long.models.length).toBeGreaterThan(1 + MAX_ALTERNATIVES);
|
|
const hw = mac(512, 'Apple M5 Max');
|
|
const offered = recommend(hw, long, harnessFacts());
|
|
const listed = new Set([offered.pick, ...offered.alternatives].map((row) => row?.modelId));
|
|
expect(listed.size).toBe(1 + MAX_ALTERNATIVES);
|
|
const unlisted = long.models.filter((model) => !listed.has(model.id));
|
|
expect(unlisted.length).toBe(long.models.length - 1 - MAX_ALTERNATIVES);
|
|
for (const model of unlisted) {
|
|
expect(modelFitsBudget(hw, long, model.id, model.contexts.at(-1) as number), model.id).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('rules out a model that is too large, and one the catalogue does not have', () => {
|
|
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);
|
|
});
|
|
|
|
it('judges against the budget the runtime measured, when it is given one', () => {
|
|
const id = 'qwen3.6:35b-a3b';
|
|
// The MacBook: the runtime reported 37.4 GiB, and the model needs 36.0 of those 40.2 GB.
|
|
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: 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 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, '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', () => {
|
|
for (const [gb, measuredGib] of [
|
|
[16, 11.8],
|
|
[32, 20],
|
|
[48, 37.4],
|
|
[48, 30],
|
|
[64, 50],
|
|
]) {
|
|
const hw = mac(gb);
|
|
const options = { measuredTotalBytes: Math.floor(measuredGib * 1024 ** 3) };
|
|
const offered = recommend(hw, catalog, harnessFacts(), options);
|
|
expect(offered.verdict === 'none' || offered.budgetSource === 'measured').toBe(true);
|
|
for (const row of [offered.pick, ...offered.alternatives].filter((entry) => entry !== undefined)) {
|
|
expect(modelFitsBudget(hw, catalog, row.modelId, row.context, options), `${gb} GB ${row.modelId}`).toBe(true);
|
|
// The next larger context of the same model does not fit, or the row would be at it.
|
|
const model = catalog.models.find((entry) => entry.id === row.modelId);
|
|
const larger = model?.contexts.filter((ctx) => ctx > row.context) ?? [];
|
|
for (const ctx of larger) expect(modelFitsBudget(hw, catalog, row.modelId, ctx, options)).toBe(false);
|
|
}
|
|
}
|
|
});
|
|
|
|
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-q4_k_m', context), String(context)).toBe(false);
|
|
}
|
|
});
|
|
});
|