feat(models): strong/cheap/vision model slots + multi-provider engine federation
Model slots wave (2026-09-20), chunk B:
- settings: new `models` section {strong, cheap, vision} slots (each
field independent, same shape as a role override); ROLE_SLOT maps all
nine roles to tiers (strong=prober+teacher, cheap=researcher/
demoBuilder/extractor/auditor/labeler, vision=visualMaker+pdfReader);
resolution chain per role: override → slot → factory pin.
- config: ModelSpec gains provider (all pins openrouter, the BYOK
default); MODEL_PINS in stores.ts now spans all nine roles (the strong
slot's factory rep IS the prober pin).
- engine: piModels() registers EVERY builtin provider catalog;
piEngineForSpec(resolved spec) is the new engine factory (pi-ai
resolves per-provider keys from env itself); piEngineFor stays as the
config-pin fallback. chat-handler resolves the conversation chain
(chat override → strong slot → pin) and injects engineFor into the job
runner, closing the old gap where per-role overrides never reached
job engines.
- /api/models: models federate ONLY keyed providers (providers surface
with keyPresent, env-wins, fresh per call); slots (resolved tier pins)
join rolePins; catalogDefaultsFor inherits the strong slot; the models
settings section gets the same catalog gate as chat.
- pricing: PRICES/CONTEXT_LIMITS uniformly keyed provider:id
(regenerated from ALL pi-ai catalogs, 1354 keys); usage stamps
(TurnUsage, JobUsage, extraction stages) switch to provider:id —
owner decision: no legacy compat, old stamps render unpriced.
This commit is contained in:
@@ -11,6 +11,7 @@ import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
parseAgentSettingsBody,
|
||||
parseModelsSettingsBody,
|
||||
parseAppearanceSettingsBody,
|
||||
parseGeneralSettingsBody,
|
||||
parseGraphSettingsBody,
|
||||
@@ -158,7 +159,47 @@ describe("parseGraphSettingsBody", () => {
|
||||
});
|
||||
|
||||
describe("SETTINGS_SECTIONS (the route's section map)", () => {
|
||||
it("maps exactly the five record sections", () => {
|
||||
assert.deepEqual(Object.keys(SETTINGS_SECTIONS), ["general", "chat", "agent", "appearance", "graph"]);
|
||||
it("maps exactly the six record sections", () => {
|
||||
assert.deepEqual(Object.keys(SETTINGS_SECTIONS), ["general", "chat", "agent", "appearance", "graph", "models"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseModelsSettingsBody (model slots wave, 2026-09-20)", () => {
|
||||
it("parses the three slot tiers, each field independent", () => {
|
||||
const r = parseModelsSettingsBody({
|
||||
strong: { model: { provider: "openrouter", id: "z-ai/glm-5.3-flash" } },
|
||||
cheap: { thinkingLevel: "high" },
|
||||
vision: { model: { provider: "zai", id: "glm-5.3-flash" }, thinkingLevel: "low" },
|
||||
});
|
||||
assert.ok(r.ok);
|
||||
assert.deepEqual(r.settings, {
|
||||
strong: { model: { provider: "openrouter", id: "z-ai/glm-5.3-flash" } },
|
||||
cheap: { thinkingLevel: "high" },
|
||||
vision: { model: { provider: "zai", id: "glm-5.3-flash" }, thinkingLevel: "low" },
|
||||
});
|
||||
});
|
||||
|
||||
it("{} is the legal clear-slots write", () => {
|
||||
const r = parseModelsSettingsBody({});
|
||||
assert.ok(r.ok);
|
||||
assert.deepEqual(r.settings, {});
|
||||
});
|
||||
|
||||
it("unknown slots are 400s with the supported list", () => {
|
||||
const bad = parseModelsSettingsBody({ mega: { thinkingLevel: "high" } });
|
||||
assert.equal(bad.ok, false);
|
||||
if (!bad.ok) assert.match(bad.error, /unknown slot "mega" \(supported: strong, cheap, vision\)/);
|
||||
});
|
||||
|
||||
it("a present-but-empty slot object is rejected (absent slot = unset)", () => {
|
||||
const bad = parseModelsSettingsBody({ strong: {} });
|
||||
assert.equal(bad.ok, false);
|
||||
if (!bad.ok) assert.match(bad.error, /slot "strong" must set model or thinkingLevel/);
|
||||
});
|
||||
|
||||
it("malformed slot shapes are 400s", () => {
|
||||
assert.equal(parseModelsSettingsBody({ cheap: { model: { provider: "x" } } }).ok, false);
|
||||
assert.equal(parseModelsSettingsBody({ vision: { thinkingLevel: 7 } }).ok, false);
|
||||
assert.equal(parseModelsSettingsBody("nope").ok, false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user