feat: add per-session model selection

This commit is contained in:
Peter Steinberger
2025-12-23 23:45:20 +00:00
parent b6bfd8e34f
commit 364a6a9444
34 changed files with 729 additions and 300 deletions

View File

@@ -49,9 +49,12 @@ function mockConfig(
inboundOverrides?: Partial<NonNullable<ClawdisConfig["inbound"]>>,
) {
configSpy.mockReturnValue({
inbound: {
agent: {
provider: "anthropic",
model: "claude-opus-4-5",
workspace: path.join(home, "clawd"),
agent: { provider: "anthropic", model: "claude-opus-4-5" },
},
inbound: {
session: { store: storePath, mainKey: "main" },
...inboundOverrides,
},

View File

@@ -5,6 +5,8 @@ import {
DEFAULT_MODEL,
DEFAULT_PROVIDER,
} from "../agents/defaults.js";
import { loadModelCatalog } from "../agents/model-catalog.js";
import { buildAllowedModelSet, modelKey } from "../agents/model-selection.js";
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
import { buildWorkspaceSkillSnapshot } from "../agents/skills.js";
import {
@@ -140,8 +142,8 @@ export async function agentCommand(
}
const cfg = loadConfig();
const agentCfg = cfg.inbound?.agent;
const workspaceDirRaw = cfg.inbound?.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR;
const agentCfg = cfg.agent;
const workspaceDirRaw = cfg.agent?.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR;
const workspace = await ensureAgentWorkspace({
dir: workspaceDirRaw,
ensureBootstrapFiles: true,
@@ -245,8 +247,53 @@ export async function agentCommand(
await saveSessionStore(storePath, sessionStore);
}
const provider = agentCfg?.provider?.trim() || DEFAULT_PROVIDER;
const model = agentCfg?.model?.trim() || DEFAULT_MODEL;
const defaultProvider = agentCfg?.provider?.trim() || DEFAULT_PROVIDER;
const defaultModel = agentCfg?.model?.trim() || DEFAULT_MODEL;
let provider = defaultProvider;
let model = defaultModel;
const hasAllowlist = (agentCfg?.allowedModels?.length ?? 0) > 0;
const hasStoredOverride = Boolean(
sessionEntry?.modelOverride || sessionEntry?.providerOverride,
);
const needsModelCatalog = hasAllowlist || hasStoredOverride;
let allowedModelKeys = new Set<string>();
if (needsModelCatalog) {
const catalog = await loadModelCatalog({ config: cfg });
const allowed = buildAllowedModelSet({
cfg,
catalog,
defaultProvider,
});
allowedModelKeys = allowed.allowedKeys;
}
if (sessionEntry && sessionStore && sessionKey && hasStoredOverride) {
const overrideProvider =
sessionEntry.providerOverride?.trim() || defaultProvider;
const overrideModel = sessionEntry.modelOverride?.trim();
if (overrideModel) {
const key = modelKey(overrideProvider, overrideModel);
if (allowedModelKeys.size > 0 && !allowedModelKeys.has(key)) {
delete sessionEntry.providerOverride;
delete sessionEntry.modelOverride;
sessionEntry.updatedAt = Date.now();
sessionStore[sessionKey] = sessionEntry;
await saveSessionStore(storePath, sessionStore);
}
}
}
const storedProviderOverride = sessionEntry?.providerOverride?.trim();
const storedModelOverride = sessionEntry?.modelOverride?.trim();
if (storedModelOverride) {
const candidateProvider = storedProviderOverride || defaultProvider;
const key = modelKey(candidateProvider, storedModelOverride);
if (allowedModelKeys.size === 0 || allowedModelKeys.has(key)) {
provider = candidateProvider;
model = storedModelOverride;
}
}
const sessionFile = resolveSessionTranscriptPath(sessionId);
const startedAt = Date.now();

View File

@@ -9,9 +9,7 @@ process.env.FORCE_COLOR = "0";
vi.mock("../config/config.js", () => ({
loadConfig: () => ({
inbound: {
agent: { model: "pi:opus", contextTokens: 32000 },
},
agent: { model: "pi:opus", contextTokens: 32000 },
}),
}));

View File

@@ -152,10 +152,10 @@ export async function sessionsCommand(
) {
const cfg = loadConfig();
const configContextTokens =
cfg.inbound?.agent?.contextTokens ??
lookupContextTokens(cfg.inbound?.agent?.model) ??
cfg.agent?.contextTokens ??
lookupContextTokens(cfg.agent?.model) ??
DEFAULT_CONTEXT_TOKENS;
const configModel = cfg.inbound?.agent?.model ?? DEFAULT_MODEL;
const configModel = cfg.agent?.model ?? DEFAULT_MODEL;
const storePath = resolveStorePath(opts.store ?? cfg.inbound?.session?.store);
const store = loadSessionStore(storePath);

View File

@@ -46,24 +46,25 @@ export async function setupCommand(
const existingRaw = await readConfigFileRaw();
const cfg = existingRaw.parsed;
const inbound = cfg.inbound ?? {};
const agent = cfg.agent ?? {};
const workspace =
desiredWorkspace ?? inbound.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR;
desiredWorkspace ?? agent.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR;
const next: ClawdisConfig = {
...cfg,
inbound: {
...inbound,
agent: {
...agent,
workspace,
},
};
if (!existingRaw.exists || inbound.workspace !== workspace) {
if (!existingRaw.exists || agent.workspace !== workspace) {
await writeConfigFile(next);
runtime.log(
!existingRaw.exists
? `Wrote ${CONFIG_PATH_CLAWDIS}`
: `Updated ${CONFIG_PATH_CLAWDIS} (set inbound.workspace)`,
: `Updated ${CONFIG_PATH_CLAWDIS} (set agent.workspace)`,
);
} else {
runtime.log(`Config OK: ${CONFIG_PATH_CLAWDIS}`);

View File

@@ -60,9 +60,9 @@ export async function getStatusSummary(): Promise<StatusSummary> {
const providerSummary = await buildProviderSummary(cfg);
const queuedSystemEvents = peekSystemEvents();
const configModel = cfg.inbound?.agent?.model ?? DEFAULT_MODEL;
const configModel = cfg.agent?.model ?? DEFAULT_MODEL;
const configContextTokens =
cfg.inbound?.agent?.contextTokens ??
cfg.agent?.contextTokens ??
lookupContextTokens(configModel) ??
DEFAULT_CONTEXT_TOKENS;