refactor(config): drop agent.provider
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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 (don’t rely on defaults).
|
Clawdis should **always pass** `--model` when invoking the embedded agent (don’t 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 won’t respond until auth is configured.
|
If the user skips auth, onboarding should be clear: the agent likely won’t respond until auth is configured.
|
||||||
|
|||||||
53
src/agents/model-selection.test.ts
Normal file
53
src/agents/model-selection.test.ts
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user