refactor(config): drop agent.provider

This commit is contained in:
Peter Steinberger
2025-12-26 00:43:44 +01:00
parent 8b815bce94
commit 1ef888ca23
14 changed files with 98 additions and 61 deletions

View File

@@ -117,8 +117,7 @@ Example:
{ {
logging: { level: "info" }, logging: { level: "info" },
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
workspace: "~/clawd", workspace: "~/clawd",
thinkingDefault: "high", thinkingDefault: "high",
timeoutSeconds: 1800, timeoutSeconds: 1800,

View File

@@ -113,7 +113,7 @@ Controls inbound/outbound prefixes and timestamps.
### `agent` ### `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 `allowedModels` lets `/model` list/filter and enforce a per-session allowlist
(omit to show the full catalog). (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`). `agent.model` should be set as `provider/model` (e.g. `anthropic/claude-opus-4-5`).
When present, it overrides `agent.provider` (which becomes optional). If you omit the provider, CLAWDIS currently assumes `anthropic` as a temporary
deprecation fallback.
`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)
@@ -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) - default behavior: **merge** (keeps existing providers, overrides on name)
- set `models.mode: "replace"` to overwrite the file contents - 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 ```json5
{ {
agent: { provider: "custom-proxy", model: "llama-3.1-8b" }, agent: { model: "custom-proxy/llama-3.1-8b" },
models: { models: {
mode: "merge", mode: "merge",
providers: { providers: {

View File

@@ -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). 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 (dont rely on defaults). Clawdis should **always pass** `--model` when invoking the embedded agent (dont rely on defaults).
Example (CLI): Example (CLI):
```bash ```bash
clawdis agent --mode rpc --provider anthropic --model claude-opus-4-5 "<message>" clawdis agent --mode rpc --model anthropic/claude-opus-4-5 "<message>"
``` ```
If the user skips auth, onboarding should be clear: the agent likely wont respond until auth is configured. If the user skips auth, onboarding should be clear: the agent likely wont respond until auth is configured.

View File

@@ -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,
});
});
});

View File

@@ -31,15 +31,17 @@ export function resolveConfiguredModelRef(params: {
defaultProvider: string; defaultProvider: string;
defaultModel: string; defaultModel: string;
}): ModelRef { }): ModelRef {
const rawProvider = params.cfg.agent?.provider?.trim() || "";
const rawModel = params.cfg.agent?.model?.trim() || ""; const rawModel = params.cfg.agent?.model?.trim() || "";
const providerFallback = rawProvider || params.defaultProvider;
if (rawModel) { if (rawModel) {
const parsed = parseModelRef(rawModel, providerFallback); const trimmed = rawModel.trim();
if (parsed) return parsed; if (trimmed.includes("/")) {
return { provider: providerFallback, model: rawModel }; 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: { export function buildAllowedModelSet(params: {

View File

@@ -102,8 +102,7 @@ describe("directive parsing", () => {
{}, {},
{ {
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
}, },
routing: { routing: {
@@ -130,8 +129,7 @@ describe("directive parsing", () => {
{}, {},
{ {
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
}, },
session: { store: path.join(home, "sessions.json") }, session: { store: path.join(home, "sessions.json") },
@@ -183,8 +181,7 @@ describe("directive parsing", () => {
{}, {},
{ {
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
}, },
routing: { routing: {
@@ -245,8 +242,7 @@ describe("directive parsing", () => {
{}, {},
{ {
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
}, },
routing: { routing: {
@@ -274,8 +270,7 @@ describe("directive parsing", () => {
{}, {},
{ {
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
allowedModels: ["anthropic/claude-opus-4-5", "openai/gpt-4.1-mini"], allowedModels: ["anthropic/claude-opus-4-5", "openai/gpt-4.1-mini"],
}, },
@@ -301,8 +296,7 @@ describe("directive parsing", () => {
{}, {},
{ {
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
allowedModels: ["openai/gpt-4.1-mini"], allowedModels: ["openai/gpt-4.1-mini"],
}, },
@@ -340,8 +334,7 @@ describe("directive parsing", () => {
{}, {},
{ {
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
allowedModels: ["openai/gpt-4.1-mini"], allowedModels: ["openai/gpt-4.1-mini"],
}, },

View File

@@ -35,8 +35,7 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
function makeCfg(home: string) { function makeCfg(home: string) {
return { return {
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
workspace: join(home, "clawd"), workspace: join(home, "clawd"),
}, },
routing: { routing: {
@@ -168,8 +167,7 @@ describe("trigger handling", () => {
{}, {},
{ {
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
workspace: join(home, "clawd"), workspace: join(home, "clawd"),
}, },
routing: { routing: {
@@ -210,8 +208,7 @@ describe("trigger handling", () => {
{}, {},
{ {
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
workspace: join(home, "clawd"), workspace: join(home, "clawd"),
}, },
routing: { routing: {
@@ -250,8 +247,7 @@ describe("trigger handling", () => {
{}, {},
{ {
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
workspace: join(home, "clawd"), workspace: join(home, "clawd"),
}, },
routing: { routing: {

View File

@@ -730,7 +730,6 @@ export async function getReplyFromConfig(
const heartbeatSeconds = resolveHeartbeatSeconds(cfg, undefined); const heartbeatSeconds = resolveHeartbeatSeconds(cfg, undefined);
const statusText = buildStatusMessage({ const statusText = buildStatusMessage({
agent: { agent: {
provider,
model, model,
contextTokens, contextTokens,
thinkingDefault: agentCfg?.thinkingDefault, thinkingDefault: agentCfg?.thinkingDefault,

View File

@@ -11,7 +11,10 @@ afterEach(() => {
describe("buildStatusMessage", () => { describe("buildStatusMessage", () => {
it("summarizes agent readiness and context usage", () => { it("summarizes agent readiness and context usage", () => {
const text = buildStatusMessage({ const text = buildStatusMessage({
agent: { provider: "anthropic", model: "pi:opus", contextTokens: 32_000 }, agent: {
model: "anthropic/pi:opus",
contextTokens: 32_000,
},
sessionEntry: { sessionEntry: {
sessionId: "abc", sessionId: "abc",
updatedAt: 0, updatedAt: 0,
@@ -112,8 +115,7 @@ describe("buildStatusMessage", () => {
const text = buildStatusMessageDynamic({ const text = buildStatusMessageDynamic({
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
contextTokens: 32_000, contextTokens: 32_000,
}, },
sessionEntry: { sessionEntry: {

View File

@@ -202,11 +202,9 @@ export function buildStatusMessage(args: StatusArgs): string {
const optionsLine = `Options: thinking=${thinkLevel} | verbose=${verboseLevel} (set with /think <level>, /verbose on|off, /model <id>)`; const optionsLine = `Options: thinking=${thinkLevel} | verbose=${verboseLevel} (set with /think <level>, /verbose on|off, /model <id>)`;
const modelLabel = args.agent?.provider?.trim() const modelLabel = model
? `${args.agent.provider}/${args.agent?.model ?? model}` ? `${resolved.provider}/${model}`
: model : "unknown";
? model
: "unknown";
const agentLine = `Agent: embedded pi • ${modelLabel}`; const agentLine = `Agent: embedded pi • ${modelLabel}`;

View File

@@ -51,8 +51,7 @@ function mockConfig(
) { ) {
configSpy.mockReturnValue({ configSpy.mockReturnValue({
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
...agentOverrides, ...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) => { await withTempHome(async (home) => {
const store = path.join(home, "sessions.json"); const store = path.join(home, "sessions.json");
mockConfig(home, store, undefined, { mockConfig(home, store, undefined, {
provider: "openai", model: "openai/gpt-4.1-mini",
model: "anthropic/claude-opus-4-5",
}); });
await agentCommand({ message: "hi", to: "+1555" }, runtime); await agentCommand({ message: "hi", to: "+1555" }, runtime);
const callArgs = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]; const callArgs = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0];
expect(callArgs?.provider).toBe("anthropic"); expect(callArgs?.provider).toBe("openai");
expect(callArgs?.model).toBe("claude-opus-4-5"); expect(callArgs?.model).toBe("gpt-4.1-mini");
}); });
}); });

View File

@@ -306,8 +306,7 @@ export type ClawdisConfig = {
models?: ModelsConfig; models?: ModelsConfig;
agent?: { agent?: {
/** Provider id, e.g. "anthropic" or "openai" (pi-ai catalog). */ /** Provider id, e.g. "anthropic" or "openai" (pi-ai catalog). */
provider?: string; /** Model id (provider/model), e.g. "anthropic/claude-opus-4-5". */
/** Model id within provider, e.g. "claude-opus-4-5". */
model?: string; model?: string;
/** Agent working directory (preferred). Used as the default cwd for agent runs. */ /** Agent working directory (preferred). Used as the default cwd for agent runs. */
workspace?: string; workspace?: string;
@@ -565,7 +564,6 @@ const ClawdisSchema = z.object({
models: ModelsConfigSchema, models: ModelsConfigSchema,
agent: z agent: z
.object({ .object({
provider: z.string().optional(),
model: z.string().optional(), model: z.string().optional(),
workspace: z.string().optional(), workspace: z.string().optional(),
allowedModels: z.array(z.string()).optional(), allowedModels: z.array(z.string()).optional(),

View File

@@ -53,8 +53,7 @@ async function writeSessionStore(home: string) {
function makeCfg(home: string, storePath: string): ClawdisConfig { function makeCfg(home: string, storePath: string): ClawdisConfig {
return { return {
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
}, },
session: { store: storePath, mainKey: "main" }, session: { store: storePath, mainKey: "main" },

View File

@@ -191,8 +191,7 @@ vi.mock("../config/config.js", () => {
CONFIG_PATH_CLAWDIS: resolveConfigPath(), CONFIG_PATH_CLAWDIS: resolveConfigPath(),
loadConfig: () => ({ loadConfig: () => ({
agent: { agent: {
provider: "anthropic", model: "anthropic/claude-opus-4-5",
model: "claude-opus-4-5",
workspace: path.join(os.tmpdir(), "clawd-gateway-test"), workspace: path.join(os.tmpdir(), "clawd-gateway-test"),
}, },
routing: { routing: {