diff --git a/docs/configuration.md b/docs/configuration.md index 3325e0d9c..57c14dc95 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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) diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index 798013e78..d86a5c236 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -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[]; diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index 00ac4a460..84dc83098 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -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) ?? diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts index 2af3bf78a..479e94ffd 100644 --- a/src/auto-reply/status.ts +++ b/src/auto-reply/status.ts @@ -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 ?? diff --git a/src/commands/agent.test.ts b/src/commands/agent.test.ts index aed7d923f..96bb9bc64 100644 --- a/src/commands/agent.test.ts +++ b/src/commands/agent.test.ts @@ -47,12 +47,14 @@ function mockConfig( home: string, storePath: string, routingOverrides?: Partial>, + agentOverrides?: Partial>, ) { 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({ diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 78edd5e5b..4051e61dd 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -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; diff --git a/src/commands/sessions.ts b/src/commands/sessions.ts index adad00577..538470016 100644 --- a/src/commands/sessions.ts +++ b/src/commands/sessions.ts @@ -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); diff --git a/src/commands/status.ts b/src/commands/status.ts index 2e75da381..4ae5b97bf 100644 --- a/src/commands/status.ts +++ b/src/commands/status.ts @@ -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 { 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) ?? diff --git a/src/cron/isolated-agent.ts b/src/cron/isolated-agent.ts index 767e3113d..a96e93c46 100644 --- a/src/cron/isolated-agent.ts +++ b/src/cron/isolated-agent.ts @@ -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, diff --git a/src/gateway/server.ts b/src/gateway/server.ts index ab2767ffd..39f60f031 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -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}`);