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
{
agent: {
provider: "anthropic",
model: "claude-opus-4-5",
model: "anthropic/claude-opus-4-5",
allowedModels: [
"anthropic/claude-opus-4-5",
"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:
- `backgroundMs`: time before auto-background (ms, default 20000)
- `timeoutSec`: auto-kill after this runtime (seconds, default 1800)

View File

@@ -26,6 +26,22 @@ export function parseModelRef(
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: {
cfg: ClawdisConfig;
catalog: ModelCatalogEntry[];

View File

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

View File

@@ -2,7 +2,12 @@ import fs from "node:fs";
import os from "node:os";
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 {
derivePromptTokens,
normalizeUsage,
@@ -129,7 +134,12 @@ const readUsageFromSessionLog = (
export function buildStatusMessage(args: StatusArgs): string {
const now = args.now ?? Date.now();
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 =
entry?.contextTokens ??
args.agent?.contextTokens ??

View File

@@ -47,12 +47,14 @@ function mockConfig(
home: string,
storePath: string,
routingOverrides?: Partial<NonNullable<ClawdisConfig["routing"]>>,
agentOverrides?: Partial<NonNullable<ClawdisConfig["agent"]>>,
) {
configSpy.mockReturnValue({
agent: {
provider: "anthropic",
model: "claude-opus-4-5",
workspace: path.join(home, "clawd"),
...agentOverrides,
},
session: { store: storePath, mainKey: "main" },
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 () => {
await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({

View File

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

View File

@@ -1,7 +1,12 @@
import chalk from "chalk";
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 {
loadSessionStore,
@@ -151,11 +156,16 @@ export async function sessionsCommand(
runtime: RuntimeEnv,
) {
const cfg = loadConfig();
const resolved = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const configContextTokens =
cfg.agent?.contextTokens ??
lookupContextTokens(cfg.agent?.model) ??
lookupContextTokens(resolved.model) ??
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 store = loadSessionStore(storePath);

View File

@@ -1,5 +1,10 @@
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 {
loadSessionStore,
@@ -60,7 +65,12 @@ export async function getStatusSummary(): Promise<StatusSummary> {
const providerSummary = await buildProviderSummary(cfg);
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 =
cfg.agent?.contextTokens ??
lookupContextTokens(configModel) ??

View File

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

View File

@@ -20,6 +20,7 @@ import {
type ModelCatalogEntry,
resetModelCatalogCacheForTest,
} from "../agents/model-catalog.js";
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
import { installSkill } from "../agents/skills-install.js";
import { buildWorkspaceSkillStatus } from "../agents/skills-status.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 {
const model = cfg.agent?.model ?? DEFAULT_MODEL;
const resolved = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const contextTokens =
cfg.agent?.contextTokens ??
lookupContextTokens(model) ??
lookupContextTokens(resolved.model) ??
DEFAULT_CONTEXT_TOKENS;
return { model: model ?? null, contextTokens: contextTokens ?? null };
return { model: resolved.model ?? null, contextTokens: contextTokens ?? null };
}
function listSessionsFromStore(params: {
@@ -5858,8 +5863,12 @@ export async function startGatewayServer(
});
});
const agentProvider = cfgAtStart.agent?.provider?.trim() || DEFAULT_PROVIDER;
const agentModel = cfgAtStart.agent?.model?.trim() || DEFAULT_MODEL;
const { provider: agentProvider, model: agentModel } =
resolveConfiguredModelRef({
cfg: cfgAtStart,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
log.info(`agent model: ${agentProvider}/${agentModel}`);
log.info(`listening on ws://${bindHost}:${port} (PID ${process.pid})`);
log.info(`log file: ${getResolvedLoggerSettings().file}`);