feat(local-model): GET /api/local-model and the harness facts

The read-only route behind the recommendation, and the one module that reads
the CLI registry for it.

- harness-facts.ts: one fact per catalogue harness row, resolved in the CLI
  registry at call time: the registry's label, `installed` from the existing
  availability probe (asked once per build), `installable` for a stock CLI
  whose install command for this platform needs no administrator rights, and
  `acceptsModelId`, the test a custom model launch applies to the model id.
  A row whose CLI is unknown, disabled or has no custom model mechanism gets
  no fact. No CLI id is compared with a literal.
- routes/local-model-routes.ts: answers { hardware, recommendation,
  catalogUpdated } in the usual envelope and reformats nothing. Admin only in
  multi-user mode (403), checked before anything is probed. A failed probe is
  an answer (200, the hardware's `source` says what was read); a 500 is left
  for a fault in the catalogue, the harness facts or recommend(). It writes no
  file and reads no setting.

Tests: test/local-model-harness-facts.test.ts,
test/routes/local-model-routes.test.ts (app.inject(), no port; the temp HOME
is byte-identical before and after the requests).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-06 16:27:49 +01:00
1 parent 92c20fe572
commit 34e95ceb62
4 files changed
+942 -29

No files matched your search

+61 -11
View File
@@ -1,25 +1,33 @@
/**
* @fileoverview `GET /api/local-model`: what this Mac is and which local model fits it.
*
* SKELETON: the route is registered and answers 501 until its handler is written. It will
* answer `{ success: true, data: LocalModelStatus }`, built from the hardware probe, the
* Answers `{ success: true, data: LocalModelStatus }`, built from the hardware probe, the
* catalogue, the harness facts and the pure `recommend()`:
* `{ hardware, recommendation, catalogUpdated }`.
* `{ hardware, recommendation, catalogUpdated }`. Nothing is reformatted here: every size text
* and every sentence in the payload was chosen by the modules that produced it.
*
* Rules:
* - Read-only. It downloads nothing, installs nothing, writes no file and reads no setting.
* - Never a 500 for a probe that failed: a failed probe is an answer (the hardware's `source`
* says so and the recommendation is verdict `none`).
* - A failed probe is an answer, never a 500. `readHardware()` reports a spawn that failed
* in the hardware's `source` (`os-fallback`), and a server that is not on macOS as
* `not-probed`; both go out as a 200 whose recommendation is verdict `none` or computed
* from what is known. Should the probe itself reject (it promises not to), the route logs
* it and answers with what `node:os` knows (`hardwareFromOs()`), under the same two
* `source` values.
* - A 500 is left for a fault that is not a probe: the catalogue, the harness facts or
* `recommend()` throwing. That is a bug, and it must not be dressed up as "no model fits".
* - The payload names hardware of the host, so in multi-user mode it is admin only (403), the
* same bar as `GET /api/doctor`.
* same bar as `GET /api/doctor`. The gate runs before anything is probed.
* - Every dependency is injected (`LocalModelRouteDeps`), so a route test spawns nothing and
* does not need a Mac: it passes its own hardware and harness facts.
*/
import { arch, totalmem } from 'node:os';
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import {
ApiErrorCode,
createErrorResponse,
getErrorMessage,
type ApiResponse,
type HardwareFacts,
type HarnessFact,
@@ -27,7 +35,10 @@ import {
type LocalModelStatus,
type Recommendation,
} from '../../types.js';
import { isAdmin } from '../route-helpers.js';
import { isMultiUserMode } from '../../config/multiuser.js';
import { getLocalModelCatalog } from '../../config/local-model-catalog.js';
import { formatGb, memClassGb } from '../../local-model/format.js';
import { readHardware } from '../../local-model/hardware.js';
import { buildHarnessFacts } from '../../local-model/harness-facts.js';
import { recommend } from '../../local-model/recommend.js';
@@ -43,18 +54,57 @@ export interface LocalModelRouteDeps {
export const defaultLocalModelRouteDeps: LocalModelRouteDeps = {
readHardware: () => readHardware(),
getCatalog: getLocalModelCatalog,
buildHarnessFacts,
buildHarnessFacts: (catalog) => buildHarnessFacts(catalog),
recommend,
};
/**
* The hardware answer when the probe rejected: memory and architecture from `node:os`, and the
* `source` the probe itself uses for "nothing was read from sysctl" (`os-fallback` on macOS,
* `not-probed` anywhere else). Sizes go through the one formatter, like every other size.
*/
export function hardwareFromOs(): HardwareFacts {
const memBytes = totalmem();
return {
platform: process.platform,
arch: arch(),
memBytes,
memGb: formatGb(memBytes),
memClassGb: memClassGb(memBytes),
source: process.platform === 'darwin' ? 'os-fallback' : 'not-probed',
};
}
export function registerLocalModelRoutes(app: FastifyInstance, deps: Partial<LocalModelRouteDeps> = {}): void {
const resolved: LocalModelRouteDeps = { ...defaultLocalModelRouteDeps, ...deps };
void resolved;
app.get(
'/api/local-model',
async (_req: FastifyRequest, reply: FastifyReply): Promise<ApiResponse<LocalModelStatus>> => {
reply.code(501);
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'GET /api/local-model is not implemented yet');
async (req: FastifyRequest, reply: FastifyReply): Promise<ApiResponse<LocalModelStatus>> => {
if (isMultiUserMode() && !isAdmin(req)) {
reply.code(403);
return createErrorResponse(ApiErrorCode.FORBIDDEN, 'Admin only in multi-user mode');
}
let hardware: HardwareFacts;
try {
hardware = await resolved.readHardware();
} catch (err) {
console.warn(`[local-model] The hardware probe failed, answering from node:os: ${getErrorMessage(err)}`);
hardware = hardwareFromOs();
}
try {
const catalog = resolved.getCatalog();
const harnessFacts = await resolved.buildHarnessFacts(catalog);
const recommendation = resolved.recommend(hardware, catalog, harnessFacts);
return { success: true, data: { hardware, recommendation, catalogUpdated: catalog.updated } };
} catch (err) {
reply.code(500);
return createErrorResponse(
ApiErrorCode.INTERNAL_ERROR,
`The local model recommendation failed: ${getErrorMessage(err)}`
);
}
}
);
}