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" },
agent: {
provider: "anthropic",
model: "claude-opus-4-5",
model: "anthropic/claude-opus-4-5",
workspace: "~/clawd",
thinkingDefault: "high",
timeoutSeconds: 1800,

View File

@@ -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: {

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).
### 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):
```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.

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;
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: {

View File

@@ -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"],
},

View File

@@ -35,8 +35,7 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
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: {

View File

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

View File

@@ -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: {

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 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}`;

View File

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

View File

@@ -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(),

View File

@@ -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" },

View File

@@ -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: {