feat(config): allow provider/model shorthand

This commit is contained in:
Peter Steinberger
2025-12-26 00:16:29 +01:00
parent 97539db36d
commit 8b815bce94
10 changed files with 114 additions and 23 deletions

View File

@@ -120,8 +120,7 @@ Controls the embedded agent runtime (provider/model/thinking/verbose/timeouts).
```json5 ```json5
{ {
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
allowedModels: [ allowedModels: [
"anthropic/claude-opus-4-5", "anthropic/claude-opus-4-5",
"anthropic/claude-sonnet-4-1" "anthropic/claude-sonnet-4-1"
@@ -142,6 +141,9 @@ Controls the embedded agent runtime (provider/model/thinking/verbose/timeouts).
} }
``` ```
`agent.model` can be set as `provider/model` (e.g. `anthropic/claude-opus-4-5`).
When present, it overrides `agent.provider` (which becomes optional).
`agent.bash` configures background bash defaults: `agent.bash` configures background bash defaults:
- `backgroundMs`: time before auto-background (ms, default 20000) - `backgroundMs`: time before auto-background (ms, default 20000)
- `timeoutSec`: auto-kill after this runtime (seconds, default 1800) - `timeoutSec`: auto-kill after this runtime (seconds, default 1800)

View File

@@ -26,6 +26,22 @@ export function parseModelRef(
return { provider, model }; return { provider, model };
} }
export function resolveConfiguredModelRef(params: {
cfg: ClawdisConfig;
defaultProvider: string;
defaultModel: string;
}): ModelRef {
const rawProvider = params.cfg.agent?.provider?.trim() || "";
const rawModel = params.cfg.agent?.model?.trim() || "";
const providerFallback = rawProvider || params.defaultProvider;
if (rawModel) {
const parsed = parseModelRef(rawModel, providerFallback);
if (parsed) return parsed;
return { provider: providerFallback, model: rawModel };
}
return { provider: providerFallback, model: params.defaultModel };
}
export function buildAllowedModelSet(params: { export function buildAllowedModelSet(params: {
cfg: ClawdisConfig; cfg: ClawdisConfig;
catalog: ModelCatalogEntry[]; catalog: ModelCatalogEntry[];

View File

@@ -11,6 +11,7 @@ import {
buildAllowedModelSet, buildAllowedModelSet,
modelKey, modelKey,
parseModelRef, parseModelRef,
resolveConfiguredModelRef,
} from "../agents/model-selection.js"; } from "../agents/model-selection.js";
import { import {
queueEmbeddedPiMessage, queueEmbeddedPiMessage,
@@ -168,8 +169,12 @@ export async function getReplyFromConfig(
const agentCfg = cfg.agent; const agentCfg = cfg.agent;
const sessionCfg = cfg.session; const sessionCfg = cfg.session;
const defaultProvider = agentCfg?.provider?.trim() || DEFAULT_PROVIDER; const { provider: defaultProvider, model: defaultModel } =
const defaultModel = agentCfg?.model?.trim() || DEFAULT_MODEL; resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
let provider = defaultProvider; let provider = defaultProvider;
let model = defaultModel; let model = defaultModel;
let contextTokens = let contextTokens =
@@ -1048,8 +1053,7 @@ export async function getReplyFromConfig(
if (sessionStore && sessionKey) { if (sessionStore && sessionKey) {
const usage = runResult.meta.agentMeta?.usage; const usage = runResult.meta.agentMeta?.usage;
const modelUsed = const modelUsed = runResult.meta.agentMeta?.model ?? defaultModel;
runResult.meta.agentMeta?.model ?? agentCfg?.model ?? DEFAULT_MODEL;
const contextTokensUsed = const contextTokensUsed =
agentCfg?.contextTokens ?? agentCfg?.contextTokens ??
lookupContextTokens(modelUsed) ?? lookupContextTokens(modelUsed) ??

View File

@@ -2,7 +2,12 @@ import fs from "node:fs";
import os from "node:os"; import os from "node:os";
import { lookupContextTokens } from "../agents/context.js"; import { lookupContextTokens } from "../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL } from "../agents/defaults.js"; import {
DEFAULT_CONTEXT_TOKENS,
DEFAULT_MODEL,
DEFAULT_PROVIDER,
} from "../agents/defaults.js";
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
import { import {
derivePromptTokens, derivePromptTokens,
normalizeUsage, normalizeUsage,
@@ -129,7 +134,12 @@ const readUsageFromSessionLog = (
export function buildStatusMessage(args: StatusArgs): string { export function buildStatusMessage(args: StatusArgs): string {
const now = args.now ?? Date.now(); const now = args.now ?? Date.now();
const entry = args.sessionEntry; const entry = args.sessionEntry;
let model = entry?.model ?? args.agent?.model ?? DEFAULT_MODEL; const resolved = resolveConfiguredModelRef({
cfg: { agent: args.agent ?? {} },
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
let model = entry?.model ?? resolved.model ?? DEFAULT_MODEL;
let contextTokens = let contextTokens =
entry?.contextTokens ?? entry?.contextTokens ??
args.agent?.contextTokens ?? args.agent?.contextTokens ??

View File

@@ -47,12 +47,14 @@ function mockConfig(
home: string, home: string,
storePath: string, storePath: string,
routingOverrides?: Partial<NonNullable<ClawdisConfig["routing"]>>, routingOverrides?: Partial<NonNullable<ClawdisConfig["routing"]>>,
agentOverrides?: Partial<NonNullable<ClawdisConfig["agent"]>>,
) { ) {
configSpy.mockReturnValue({ configSpy.mockReturnValue({
agent: { agent: {
provider: "anthropic", provider: "anthropic",
model: "claude-opus-4-5", model: "claude-opus-4-5",
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
...agentOverrides,
}, },
session: { store: storePath, mainKey: "main" }, session: { store: storePath, mainKey: "main" },
routing: routingOverrides ? { ...routingOverrides } : undefined, routing: routingOverrides ? { ...routingOverrides } : undefined,
@@ -141,6 +143,22 @@ describe("agentCommand", () => {
}); });
}); });
it("resolves provider from agent.model when prefixed", async () => {
await withTempHome(async (home) => {
const store = path.join(home, "sessions.json");
mockConfig(home, store, undefined, {
provider: "openai",
model: "anthropic/claude-opus-4-5",
});
await agentCommand({ message: "hi", to: "+1555" }, runtime);
const callArgs = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0];
expect(callArgs?.provider).toBe("anthropic");
expect(callArgs?.model).toBe("claude-opus-4-5");
});
});
it("prints JSON payload when requested", async () => { it("prints JSON payload when requested", async () => {
await withTempHome(async (home) => { await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ vi.mocked(runEmbeddedPiAgent).mockResolvedValue({

View File

@@ -6,7 +6,11 @@ import {
DEFAULT_PROVIDER, DEFAULT_PROVIDER,
} from "../agents/defaults.js"; } from "../agents/defaults.js";
import { loadModelCatalog } from "../agents/model-catalog.js"; import { loadModelCatalog } from "../agents/model-catalog.js";
import { buildAllowedModelSet, modelKey } from "../agents/model-selection.js"; import {
buildAllowedModelSet,
modelKey,
resolveConfiguredModelRef,
} from "../agents/model-selection.js";
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
import { buildWorkspaceSkillSnapshot } from "../agents/skills.js"; import { buildWorkspaceSkillSnapshot } from "../agents/skills.js";
import { import {
@@ -247,8 +251,12 @@ export async function agentCommand(
await saveSessionStore(storePath, sessionStore); await saveSessionStore(storePath, sessionStore);
} }
const defaultProvider = agentCfg?.provider?.trim() || DEFAULT_PROVIDER; const { provider: defaultProvider, model: defaultModel } =
const defaultModel = agentCfg?.model?.trim() || DEFAULT_MODEL; resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
let provider = defaultProvider; let provider = defaultProvider;
let model = defaultModel; let model = defaultModel;
const hasAllowlist = (agentCfg?.allowedModels?.length ?? 0) > 0; const hasAllowlist = (agentCfg?.allowedModels?.length ?? 0) > 0;

View File

@@ -1,7 +1,12 @@
import chalk from "chalk"; import chalk from "chalk";
import { lookupContextTokens } from "../agents/context.js"; import { lookupContextTokens } from "../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL } from "../agents/defaults.js"; import {
DEFAULT_CONTEXT_TOKENS,
DEFAULT_MODEL,
DEFAULT_PROVIDER,
} from "../agents/defaults.js";
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
import { loadConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js";
import { import {
loadSessionStore, loadSessionStore,
@@ -151,11 +156,16 @@ export async function sessionsCommand(
runtime: RuntimeEnv, runtime: RuntimeEnv,
) { ) {
const cfg = loadConfig(); const cfg = loadConfig();
const resolved = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const configContextTokens = const configContextTokens =
cfg.agent?.contextTokens ?? cfg.agent?.contextTokens ??
lookupContextTokens(cfg.agent?.model) ?? lookupContextTokens(resolved.model) ??
DEFAULT_CONTEXT_TOKENS; DEFAULT_CONTEXT_TOKENS;
const configModel = cfg.agent?.model ?? DEFAULT_MODEL; const configModel = resolved.model ?? DEFAULT_MODEL;
const storePath = resolveStorePath(opts.store ?? cfg.session?.store); const storePath = resolveStorePath(opts.store ?? cfg.session?.store);
const store = loadSessionStore(storePath); const store = loadSessionStore(storePath);

View File

@@ -1,5 +1,10 @@
import { lookupContextTokens } from "../agents/context.js"; import { lookupContextTokens } from "../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL } from "../agents/defaults.js"; import {
DEFAULT_CONTEXT_TOKENS,
DEFAULT_MODEL,
DEFAULT_PROVIDER,
} from "../agents/defaults.js";
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
import { loadConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js";
import { import {
loadSessionStore, loadSessionStore,
@@ -60,7 +65,12 @@ export async function getStatusSummary(): Promise<StatusSummary> {
const providerSummary = await buildProviderSummary(cfg); const providerSummary = await buildProviderSummary(cfg);
const queuedSystemEvents = peekSystemEvents(); const queuedSystemEvents = peekSystemEvents();
const configModel = cfg.agent?.model ?? DEFAULT_MODEL; const resolved = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const configModel = resolved.model ?? DEFAULT_MODEL;
const configContextTokens = const configContextTokens =
cfg.agent?.contextTokens ?? cfg.agent?.contextTokens ??
lookupContextTokens(configModel) ?? lookupContextTokens(configModel) ??

View File

@@ -5,6 +5,7 @@ import {
DEFAULT_MODEL, DEFAULT_MODEL,
DEFAULT_PROVIDER, DEFAULT_PROVIDER,
} from "../agents/defaults.js"; } from "../agents/defaults.js";
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
import { buildWorkspaceSkillSnapshot } from "../agents/skills.js"; import { buildWorkspaceSkillSnapshot } from "../agents/skills.js";
import { import {
@@ -154,8 +155,11 @@ export async function runCronIsolatedAgentTurn(params: {
}); });
const workspaceDir = workspace.dir; const workspaceDir = workspace.dir;
const provider = agentCfg?.provider?.trim() || DEFAULT_PROVIDER; const { provider, model } = resolveConfiguredModelRef({
const model = agentCfg?.model?.trim() || DEFAULT_MODEL; cfg: params.cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const now = Date.now(); const now = Date.now();
const cronSession = resolveCronSession({ const cronSession = resolveCronSession({
cfg: params.cfg, cfg: params.cfg,

View File

@@ -20,6 +20,7 @@ import {
type ModelCatalogEntry, type ModelCatalogEntry,
resetModelCatalogCacheForTest, resetModelCatalogCacheForTest,
} from "../agents/model-catalog.js"; } from "../agents/model-catalog.js";
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
import { installSkill } from "../agents/skills-install.js"; import { installSkill } from "../agents/skills-install.js";
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js"; import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
import { DEFAULT_AGENT_WORKSPACE_DIR } from "../agents/workspace.js"; import { DEFAULT_AGENT_WORKSPACE_DIR } from "../agents/workspace.js";
@@ -865,12 +866,16 @@ function classifySessionKey(key: string): GatewaySessionRow["kind"] {
} }
function getSessionDefaults(cfg: ClawdisConfig): GatewaySessionsDefaults { function getSessionDefaults(cfg: ClawdisConfig): GatewaySessionsDefaults {
const model = cfg.agent?.model ?? DEFAULT_MODEL; const resolved = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const contextTokens = const contextTokens =
cfg.agent?.contextTokens ?? cfg.agent?.contextTokens ??
lookupContextTokens(model) ?? lookupContextTokens(resolved.model) ??
DEFAULT_CONTEXT_TOKENS; DEFAULT_CONTEXT_TOKENS;
return { model: model ?? null, contextTokens: contextTokens ?? null }; return { model: resolved.model ?? null, contextTokens: contextTokens ?? null };
} }
function listSessionsFromStore(params: { function listSessionsFromStore(params: {
@@ -5858,8 +5863,12 @@ export async function startGatewayServer(
}); });
}); });
const agentProvider = cfgAtStart.agent?.provider?.trim() || DEFAULT_PROVIDER; const { provider: agentProvider, model: agentModel } =
const agentModel = cfgAtStart.agent?.model?.trim() || DEFAULT_MODEL; resolveConfiguredModelRef({
cfg: cfgAtStart,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
log.info(`agent model: ${agentProvider}/${agentModel}`); log.info(`agent model: ${agentProvider}/${agentModel}`);
log.info(`listening on ws://${bindHost}:${port} (PID ${process.pid})`); log.info(`listening on ws://${bindHost}:${port} (PID ${process.pid})`);
log.info(`log file: ${getResolvedLoggerSettings().file}`); log.info(`log file: ${getResolvedLoggerSettings().file}`);