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:
@@ -19,9 +19,10 @@ import type {
|
||||
BackgroundModelRole,
|
||||
GeneralSettings,
|
||||
GraphSettings,
|
||||
ModelsSettings,
|
||||
RoleModelOverride,
|
||||
} from "../core/settings.ts";
|
||||
import { BACKGROUND_MODEL_ROLES } from "../core/settings.ts";
|
||||
import { BACKGROUND_MODEL_ROLES, MODEL_SLOTS } from "../core/settings.ts";
|
||||
import { TOGGLEABLE_TOOLS } from "../core/pending.ts";
|
||||
|
||||
/** Parsed-or-rejected — `ok: false` carries the 400's error message. */
|
||||
@@ -292,6 +293,31 @@ export function parseGraphSettingsBody(body: unknown): SettingsBodyResult<GraphS
|
||||
return { ok: true, settings };
|
||||
}
|
||||
|
||||
/** models: { strong?, cheap?, vision? } — the slot tiers (model slots
|
||||
* wave, 2026-09-20). Each slot is the same shape as a role override; a
|
||||
* present-but-empty slot object is rejected (absent slot = unset; say
|
||||
* what you mean). Catalog validity is checked by the route
|
||||
* (validateChatSettings discipline). */
|
||||
export function parseModelsSettingsBody(body: unknown): SettingsBodyResult<ModelsSettings> {
|
||||
const raw = asRecord(body);
|
||||
if (!raw) return { ok: false, error: "Invalid JSON body" };
|
||||
const settings: ModelsSettings = {};
|
||||
for (const key of Object.keys(raw)) {
|
||||
const known = MODEL_SLOTS as readonly string[];
|
||||
if (!known.includes(key)) {
|
||||
return { ok: false, error: `unknown slot "${key}" (supported: ${known.join(", ")})` };
|
||||
}
|
||||
const parsed = parseRoleOverride(raw[key], key);
|
||||
if (!parsed.ok) return { ok: false, error: parsed.error };
|
||||
if (Object.keys(parsed.override).length) {
|
||||
settings[key as (typeof MODEL_SLOTS)[number]] = parsed.override;
|
||||
} else {
|
||||
return { ok: false, error: `slot "${key}" must set model or thinkingLevel (absent slot = unset)` };
|
||||
}
|
||||
}
|
||||
return { ok: true, settings };
|
||||
}
|
||||
|
||||
/** The section→parser map for PUT /api/settings/[section] (the route also
|
||||
* carries the special prompts sub-resource). */
|
||||
export const SETTINGS_SECTIONS = {
|
||||
@@ -300,6 +326,7 @@ export const SETTINGS_SECTIONS = {
|
||||
agent: parseAgentSettingsBody,
|
||||
appearance: parseAppearanceSettingsBody,
|
||||
graph: parseGraphSettingsBody,
|
||||
models: parseModelsSettingsBody,
|
||||
} as const;
|
||||
|
||||
export type SettingsSectionName = keyof typeof SETTINGS_SECTIONS;
|
||||
|
||||
Reference in New Issue
Block a user