diff --git a/docs/clawd.md b/docs/clawd.md index 9b2e699cd..893a70ebb 100644 --- a/docs/clawd.md +++ b/docs/clawd.md @@ -117,8 +117,7 @@ Example: { logging: { level: "info" }, agent: { - provider: "anthropic", - model: "claude-opus-4-5", + model: "anthropic/claude-opus-4-5", workspace: "~/clawd", thinkingDefault: "high", timeoutSeconds: 1800, diff --git a/docs/configuration.md b/docs/configuration.md index 57c14dc95..4b407feca 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -113,7 +113,7 @@ Controls inbound/outbound prefixes and timestamps. ### `agent` -Controls the embedded agent runtime (provider/model/thinking/verbose/timeouts). +Controls the embedded agent runtime (model/thinking/verbose/timeouts). `allowedModels` lets `/model` list/filter and enforce a per-session allowlist (omit to show the full catalog). @@ -141,8 +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.model` should be set as `provider/model` (e.g. `anthropic/claude-opus-4-5`). +If you omit the provider, CLAWDIS currently assumes `anthropic` as a temporary +deprecation fallback. `agent.bash` configures background bash defaults: - `backgroundMs`: time before auto-background (ms, default 20000) @@ -165,11 +166,11 @@ When `models.providers` is present, Clawdis writes/merges a `models.json` into - default behavior: **merge** (keeps existing providers, overrides on name) - set `models.mode: "replace"` to overwrite the file contents -Select the model via `agent.provider` + `agent.model`. +Select the model via `agent.model` (provider/model). ```json5 { - agent: { provider: "custom-proxy", model: "llama-3.1-8b" }, + agent: { model: "custom-proxy/llama-3.1-8b" }, models: { mode: "merge", providers: { diff --git a/docs/onboarding.md b/docs/onboarding.md index 9b536e0eb..958a953f3 100644 --- a/docs/onboarding.md +++ b/docs/onboarding.md @@ -47,14 +47,14 @@ Offer an “API key” option, but for now it is **instructions only**: Note: environment variables are often confusing when the Gateway is launched by a GUI app (launchd environment != your shell). -### Provider/model safety rule +### Model safety rule -Clawdis should **always pass** `--provider` and `--model` when invoking the embedded agent (don’t rely on defaults). +Clawdis should **always pass** `--model` when invoking the embedded agent (don’t rely on defaults). Example (CLI): ```bash -clawdis agent --mode rpc --provider anthropic --model claude-opus-4-5 "" +clawdis agent --mode rpc --model anthropic/claude-opus-4-5 "" ``` If the user skips auth, onboarding should be clear: the agent likely won’t respond until auth is configured. diff --git a/src/agents/model-selection.test.ts b/src/agents/model-selection.test.ts new file mode 100644 index 000000000..385a7de65 --- /dev/null +++ b/src/agents/model-selection.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; + +import type { ClawdisConfig } from "../config/config.js"; +import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js"; +import { resolveConfiguredModelRef } from "./model-selection.js"; + +describe("resolveConfiguredModelRef", () => { + it("parses provider/model from agent.model", () => { + const cfg = { + agent: { model: "openai/gpt-4.1-mini" }, + } satisfies ClawdisConfig; + + const resolved = resolveConfiguredModelRef({ + cfg, + defaultProvider: DEFAULT_PROVIDER, + defaultModel: DEFAULT_MODEL, + }); + + expect(resolved).toEqual({ provider: "openai", model: "gpt-4.1-mini" }); + }); + + it("falls back to default provider when agent.model omits it", () => { + const cfg = { + agent: { model: "claude-opus-4-5" }, + } satisfies ClawdisConfig; + + const resolved = resolveConfiguredModelRef({ + cfg, + defaultProvider: DEFAULT_PROVIDER, + defaultModel: DEFAULT_MODEL, + }); + + expect(resolved).toEqual({ + provider: DEFAULT_PROVIDER, + model: "claude-opus-4-5", + }); + }); + + it("falls back to defaults when agent.model is missing", () => { + const cfg = {} satisfies ClawdisConfig; + + const resolved = resolveConfiguredModelRef({ + cfg, + defaultProvider: DEFAULT_PROVIDER, + defaultModel: DEFAULT_MODEL, + }); + + expect(resolved).toEqual({ + provider: DEFAULT_PROVIDER, + model: DEFAULT_MODEL, + }); + }); +}); diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index d86a5c236..92f2392b9 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -31,15 +31,17 @@ export function resolveConfiguredModelRef(params: { 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 }; + const trimmed = rawModel.trim(); + if (trimmed.includes("/")) { + const parsed = parseModelRef(trimmed, params.defaultProvider); + if (parsed) return parsed; + } + // TODO(steipete): drop this fallback once provider-less agent.model is fully deprecated. + return { provider: params.defaultProvider, model: trimmed }; } - return { provider: providerFallback, model: params.defaultModel }; + return { provider: params.defaultProvider, model: params.defaultModel }; } export function buildAllowedModelSet(params: { diff --git a/src/auto-reply/reply.directive.test.ts b/src/auto-reply/reply.directive.test.ts index faf1a0493..f8f43cde9 100644 --- a/src/auto-reply/reply.directive.test.ts +++ b/src/auto-reply/reply.directive.test.ts @@ -102,8 +102,7 @@ describe("directive parsing", () => { {}, { agent: { - provider: "anthropic", - model: "claude-opus-4-5", + model: "anthropic/claude-opus-4-5", workspace: path.join(home, "clawd"), }, routing: { @@ -130,8 +129,7 @@ describe("directive parsing", () => { {}, { agent: { - provider: "anthropic", - model: "claude-opus-4-5", + model: "anthropic/claude-opus-4-5", workspace: path.join(home, "clawd"), }, session: { store: path.join(home, "sessions.json") }, @@ -183,8 +181,7 @@ describe("directive parsing", () => { {}, { agent: { - provider: "anthropic", - model: "claude-opus-4-5", + model: "anthropic/claude-opus-4-5", workspace: path.join(home, "clawd"), }, routing: { @@ -245,8 +242,7 @@ describe("directive parsing", () => { {}, { agent: { - provider: "anthropic", - model: "claude-opus-4-5", + model: "anthropic/claude-opus-4-5", workspace: path.join(home, "clawd"), }, routing: { @@ -274,8 +270,7 @@ describe("directive parsing", () => { {}, { agent: { - provider: "anthropic", - model: "claude-opus-4-5", + model: "anthropic/claude-opus-4-5", workspace: path.join(home, "clawd"), allowedModels: ["anthropic/claude-opus-4-5", "openai/gpt-4.1-mini"], }, @@ -301,8 +296,7 @@ describe("directive parsing", () => { {}, { agent: { - provider: "anthropic", - model: "claude-opus-4-5", + model: "anthropic/claude-opus-4-5", workspace: path.join(home, "clawd"), allowedModels: ["openai/gpt-4.1-mini"], }, @@ -340,8 +334,7 @@ describe("directive parsing", () => { {}, { agent: { - provider: "anthropic", - model: "claude-opus-4-5", + model: "anthropic/claude-opus-4-5", workspace: path.join(home, "clawd"), allowedModels: ["openai/gpt-4.1-mini"], }, diff --git a/src/auto-reply/reply.triggers.test.ts b/src/auto-reply/reply.triggers.test.ts index 97d6c4bd0..07d1a7a58 100644 --- a/src/auto-reply/reply.triggers.test.ts +++ b/src/auto-reply/reply.triggers.test.ts @@ -35,8 +35,7 @@ async function withTempHome(fn: (home: string) => Promise): Promise { function makeCfg(home: string) { return { agent: { - provider: "anthropic", - model: "claude-opus-4-5", + model: "anthropic/claude-opus-4-5", workspace: join(home, "clawd"), }, routing: { @@ -168,8 +167,7 @@ describe("trigger handling", () => { {}, { agent: { - provider: "anthropic", - model: "claude-opus-4-5", + model: "anthropic/claude-opus-4-5", workspace: join(home, "clawd"), }, routing: { @@ -210,8 +208,7 @@ describe("trigger handling", () => { {}, { agent: { - provider: "anthropic", - model: "claude-opus-4-5", + model: "anthropic/claude-opus-4-5", workspace: join(home, "clawd"), }, routing: { @@ -250,8 +247,7 @@ describe("trigger handling", () => { {}, { agent: { - provider: "anthropic", - model: "claude-opus-4-5", + model: "anthropic/claude-opus-4-5", workspace: join(home, "clawd"), }, routing: { diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index 84dc83098..7e5ba7bf3 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -730,7 +730,6 @@ export async function getReplyFromConfig( const heartbeatSeconds = resolveHeartbeatSeconds(cfg, undefined); const statusText = buildStatusMessage({ agent: { - provider, model, contextTokens, thinkingDefault: agentCfg?.thinkingDefault, diff --git a/src/auto-reply/status.test.ts b/src/auto-reply/status.test.ts index 9873b5b6c..0ca8852ef 100644 --- a/src/auto-reply/status.test.ts +++ b/src/auto-reply/status.test.ts @@ -11,7 +11,10 @@ afterEach(() => { describe("buildStatusMessage", () => { it("summarizes agent readiness and context usage", () => { const text = buildStatusMessage({ - agent: { provider: "anthropic", model: "pi:opus", contextTokens: 32_000 }, + agent: { + model: "anthropic/pi:opus", + contextTokens: 32_000, + }, sessionEntry: { sessionId: "abc", updatedAt: 0, @@ -112,8 +115,7 @@ describe("buildStatusMessage", () => { const text = buildStatusMessageDynamic({ agent: { - provider: "anthropic", - model: "claude-opus-4-5", + model: "anthropic/claude-opus-4-5", contextTokens: 32_000, }, sessionEntry: { diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts index 479e94ffd..dd12f3479 100644 --- a/src/auto-reply/status.ts +++ b/src/auto-reply/status.ts @@ -202,11 +202,9 @@ export function buildStatusMessage(args: StatusArgs): string { const optionsLine = `Options: thinking=${thinkLevel} | verbose=${verboseLevel} (set with /think , /verbose on|off, /model )`; - const modelLabel = args.agent?.provider?.trim() - ? `${args.agent.provider}/${args.agent?.model ?? model}` - : model - ? model - : "unknown"; + const modelLabel = model + ? `${resolved.provider}/${model}` + : "unknown"; const agentLine = `Agent: embedded pi • ${modelLabel}`; diff --git a/src/commands/agent.test.ts b/src/commands/agent.test.ts index 96bb9bc64..974c9e8b2 100644 --- a/src/commands/agent.test.ts +++ b/src/commands/agent.test.ts @@ -51,8 +51,7 @@ function mockConfig( ) { configSpy.mockReturnValue({ agent: { - provider: "anthropic", - model: "claude-opus-4-5", + model: "anthropic/claude-opus-4-5", workspace: path.join(home, "clawd"), ...agentOverrides, }, @@ -143,19 +142,18 @@ describe("agentCommand", () => { }); }); - it("resolves provider from agent.model when prefixed", async () => { + it("uses provider/model from agent.model", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); mockConfig(home, store, undefined, { - provider: "openai", - model: "anthropic/claude-opus-4-5", + model: "openai/gpt-4.1-mini", }); 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"); + expect(callArgs?.provider).toBe("openai"); + expect(callArgs?.model).toBe("gpt-4.1-mini"); }); }); diff --git a/src/config/config.ts b/src/config/config.ts index 56735a3ad..1e16035c6 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -306,8 +306,7 @@ export type ClawdisConfig = { models?: ModelsConfig; agent?: { /** Provider id, e.g. "anthropic" or "openai" (pi-ai catalog). */ - provider?: string; - /** Model id within provider, e.g. "claude-opus-4-5". */ + /** Model id (provider/model), e.g. "anthropic/claude-opus-4-5". */ model?: string; /** Agent working directory (preferred). Used as the default cwd for agent runs. */ workspace?: string; @@ -565,7 +564,6 @@ const ClawdisSchema = z.object({ models: ModelsConfigSchema, agent: z .object({ - provider: z.string().optional(), model: z.string().optional(), workspace: z.string().optional(), allowedModels: z.array(z.string()).optional(), diff --git a/src/cron/isolated-agent.test.ts b/src/cron/isolated-agent.test.ts index 9b8751d49..f4bf2f36f 100644 --- a/src/cron/isolated-agent.test.ts +++ b/src/cron/isolated-agent.test.ts @@ -53,8 +53,7 @@ async function writeSessionStore(home: string) { function makeCfg(home: string, storePath: string): ClawdisConfig { return { agent: { - provider: "anthropic", - model: "claude-opus-4-5", + model: "anthropic/claude-opus-4-5", workspace: path.join(home, "clawd"), }, session: { store: storePath, mainKey: "main" }, diff --git a/src/gateway/server.test.ts b/src/gateway/server.test.ts index 58b0462b2..92fd22fe1 100644 --- a/src/gateway/server.test.ts +++ b/src/gateway/server.test.ts @@ -191,8 +191,7 @@ vi.mock("../config/config.js", () => { CONFIG_PATH_CLAWDIS: resolveConfigPath(), loadConfig: () => ({ agent: { - provider: "anthropic", - model: "claude-opus-4-5", + model: "anthropic/claude-opus-4-5", workspace: path.join(os.tmpdir(), "clawd-gateway-test"), }, routing: {