feat!: redesign model config + auth profiles
This commit is contained in:
@@ -628,6 +628,18 @@ describe("legacy config detection", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects legacy agent.model string", async () => {
|
||||
vi.resetModules();
|
||||
const { validateConfigObject } = await import("./config.js");
|
||||
const res = validateConfigObject({
|
||||
agent: { model: "anthropic/claude-opus-4-5" },
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues[0]?.path).toBe("agent.model");
|
||||
}
|
||||
});
|
||||
|
||||
it("migrates telegram.requireMention to telegram.groups.*.requireMention", async () => {
|
||||
vi.resetModules();
|
||||
const { migrateLegacyConfig } = await import("./config.js");
|
||||
@@ -641,6 +653,38 @@ describe("legacy config detection", () => {
|
||||
expect(res.config?.telegram?.requireMention).toBeUndefined();
|
||||
});
|
||||
|
||||
it("migrates legacy model config to agent.models + model lists", async () => {
|
||||
vi.resetModules();
|
||||
const { migrateLegacyConfig } = await import("./config.js");
|
||||
const res = migrateLegacyConfig({
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
modelFallbacks: ["openai/gpt-4.1-mini"],
|
||||
imageModel: "openai/gpt-4.1-mini",
|
||||
imageModelFallbacks: ["anthropic/claude-opus-4-5"],
|
||||
allowedModels: ["anthropic/claude-opus-4-5", "openai/gpt-4.1-mini"],
|
||||
modelAliases: { Opus: "anthropic/claude-opus-4-5" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.config?.agent?.model?.primary).toBe("anthropic/claude-opus-4-5");
|
||||
expect(res.config?.agent?.model?.fallbacks).toEqual([
|
||||
"openai/gpt-4.1-mini",
|
||||
]);
|
||||
expect(res.config?.agent?.imageModel?.primary).toBe("openai/gpt-4.1-mini");
|
||||
expect(res.config?.agent?.imageModel?.fallbacks).toEqual([
|
||||
"anthropic/claude-opus-4-5",
|
||||
]);
|
||||
expect(
|
||||
res.config?.agent?.models?.["anthropic/claude-opus-4-5"],
|
||||
).toMatchObject({ alias: "Opus" });
|
||||
expect(res.config?.agent?.models?.["openai/gpt-4.1-mini"]).toBeTruthy();
|
||||
expect(res.config?.agent?.allowedModels).toBeUndefined();
|
||||
expect(res.config?.agent?.modelAliases).toBeUndefined();
|
||||
expect(res.config?.agent?.modelFallbacks).toBeUndefined();
|
||||
expect(res.config?.agent?.imageModelFallbacks).toBeUndefined();
|
||||
});
|
||||
|
||||
it("surfaces legacy issues in snapshot", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||
|
||||
@@ -92,43 +92,23 @@ export function applyTalkApiKey(config: ClawdbotConfig): ClawdbotConfig {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAliasKey(value: string): string {
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function applyModelAliasDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
export function applyModelDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
const existingAgent = cfg.agent;
|
||||
if (!existingAgent) return cfg;
|
||||
const existingAliases = existingAgent?.modelAliases ?? {};
|
||||
|
||||
const byNormalized = new Map<string, string>();
|
||||
for (const key of Object.keys(existingAliases)) {
|
||||
const norm = normalizeAliasKey(key);
|
||||
if (!norm) continue;
|
||||
if (!byNormalized.has(norm)) byNormalized.set(norm, key);
|
||||
}
|
||||
const existingModels = existingAgent.models ?? {};
|
||||
if (Object.keys(existingModels).length === 0) return cfg;
|
||||
|
||||
let mutated = false;
|
||||
const nextAliases: Record<string, string> = { ...existingAliases };
|
||||
const nextModels: Record<string, { alias?: string }> = {
|
||||
...existingModels,
|
||||
};
|
||||
|
||||
for (const [canonicalKey, target] of Object.entries(DEFAULT_MODEL_ALIASES)) {
|
||||
const norm = normalizeAliasKey(canonicalKey);
|
||||
const existingKey = byNormalized.get(norm);
|
||||
|
||||
if (!existingKey) {
|
||||
nextAliases[canonicalKey] = target;
|
||||
byNormalized.set(norm, canonicalKey);
|
||||
mutated = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingValue = String(existingAliases[existingKey] ?? "");
|
||||
if (existingKey !== canonicalKey && existingValue === target) {
|
||||
delete nextAliases[existingKey];
|
||||
nextAliases[canonicalKey] = target;
|
||||
byNormalized.set(norm, canonicalKey);
|
||||
mutated = true;
|
||||
}
|
||||
for (const [alias, target] of Object.entries(DEFAULT_MODEL_ALIASES)) {
|
||||
const entry = nextModels[target];
|
||||
if (!entry) continue;
|
||||
if (entry.alias !== undefined) continue;
|
||||
nextModels[target] = { ...entry, alias };
|
||||
mutated = true;
|
||||
}
|
||||
|
||||
if (!mutated) return cfg;
|
||||
@@ -137,7 +117,7 @@ export function applyModelAliasDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
...cfg,
|
||||
agent: {
|
||||
...existingAgent,
|
||||
modelAliases: nextAliases,
|
||||
models: nextModels,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import {
|
||||
applyIdentityDefaults,
|
||||
applyLoggingDefaults,
|
||||
applyModelAliasDefaults,
|
||||
applyModelDefaults,
|
||||
applySessionDefaults,
|
||||
applyTalkApiKey,
|
||||
} from "./defaults.js";
|
||||
@@ -114,7 +114,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
}
|
||||
return {};
|
||||
}
|
||||
const cfg = applyModelAliasDefaults(
|
||||
const cfg = applyModelDefaults(
|
||||
applySessionDefaults(
|
||||
applyLoggingDefaults(
|
||||
applyIdentityDefaults(validated.data as ClawdbotConfig),
|
||||
@@ -148,7 +148,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
const exists = deps.fs.existsSync(configPath);
|
||||
if (!exists) {
|
||||
const config = applyTalkApiKey(
|
||||
applyModelAliasDefaults(applySessionDefaults({})),
|
||||
applyModelDefaults(applySessionDefaults({})),
|
||||
);
|
||||
const legacyIssues: LegacyConfigIssue[] = [];
|
||||
return {
|
||||
@@ -204,7 +204,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
parsed: parsedRes.parsed,
|
||||
valid: true,
|
||||
config: applyTalkApiKey(
|
||||
applyModelAliasDefaults(
|
||||
applyModelDefaults(
|
||||
applySessionDefaults(applyLoggingDefaults(validated.config)),
|
||||
),
|
||||
),
|
||||
@@ -229,7 +229,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
await deps.fs.promises.mkdir(path.dirname(configPath), {
|
||||
recursive: true,
|
||||
});
|
||||
const json = JSON.stringify(applyModelAliasDefaults(cfg), null, 2)
|
||||
const json = JSON.stringify(applyModelDefaults(cfg), null, 2)
|
||||
.trimEnd()
|
||||
.concat("\n");
|
||||
await deps.fs.promises.writeFile(configPath, json, "utf-8");
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { LegacyConfigIssue } from "./types.js";
|
||||
type LegacyConfigRule = {
|
||||
path: string[];
|
||||
message: string;
|
||||
match?: (value: unknown, root: Record<string, unknown>) => boolean;
|
||||
};
|
||||
|
||||
type LegacyConfigMigration = {
|
||||
@@ -27,6 +28,38 @@ const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
|
||||
message:
|
||||
'telegram.requireMention was removed; use telegram.groups."*".requireMention instead (run `clawdbot doctor` to migrate).',
|
||||
},
|
||||
{
|
||||
path: ["agent", "model"],
|
||||
message:
|
||||
"agent.model string was replaced by agent.model.primary/fallbacks and agent.models (run `clawdbot doctor` to migrate).",
|
||||
match: (value) => typeof value === "string",
|
||||
},
|
||||
{
|
||||
path: ["agent", "imageModel"],
|
||||
message:
|
||||
"agent.imageModel string was replaced by agent.imageModel.primary/fallbacks (run `clawdbot doctor` to migrate).",
|
||||
match: (value) => typeof value === "string",
|
||||
},
|
||||
{
|
||||
path: ["agent", "allowedModels"],
|
||||
message:
|
||||
"agent.allowedModels was replaced by agent.models (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["agent", "modelAliases"],
|
||||
message:
|
||||
"agent.modelAliases was replaced by agent.models.*.alias (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["agent", "modelFallbacks"],
|
||||
message:
|
||||
"agent.modelFallbacks was replaced by agent.model.fallbacks (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
{
|
||||
path: ["agent", "imageModelFallbacks"],
|
||||
message:
|
||||
"agent.imageModelFallbacks was replaced by agent.imageModel.fallbacks (run `clawdbot doctor` to migrate).",
|
||||
},
|
||||
];
|
||||
|
||||
const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [
|
||||
@@ -165,6 +198,158 @@ const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "agent.model-config-v2",
|
||||
describe:
|
||||
"Migrate legacy agent.model/allowedModels/modelAliases/modelFallbacks/imageModelFallbacks to agent.models + model lists",
|
||||
apply: (raw, changes) => {
|
||||
const agent =
|
||||
raw.agent && typeof raw.agent === "object"
|
||||
? (raw.agent as Record<string, unknown>)
|
||||
: null;
|
||||
if (!agent) return;
|
||||
|
||||
const legacyModel =
|
||||
typeof agent.model === "string" ? String(agent.model) : undefined;
|
||||
const legacyImageModel =
|
||||
typeof agent.imageModel === "string"
|
||||
? String(agent.imageModel)
|
||||
: undefined;
|
||||
const legacyAllowed = Array.isArray(agent.allowedModels)
|
||||
? (agent.allowedModels as unknown[]).map(String)
|
||||
: [];
|
||||
const legacyModelFallbacks = Array.isArray(agent.modelFallbacks)
|
||||
? (agent.modelFallbacks as unknown[]).map(String)
|
||||
: [];
|
||||
const legacyImageModelFallbacks = Array.isArray(agent.imageModelFallbacks)
|
||||
? (agent.imageModelFallbacks as unknown[]).map(String)
|
||||
: [];
|
||||
const legacyAliases =
|
||||
agent.modelAliases && typeof agent.modelAliases === "object"
|
||||
? (agent.modelAliases as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
const hasLegacy =
|
||||
legacyModel ||
|
||||
legacyImageModel ||
|
||||
legacyAllowed.length > 0 ||
|
||||
legacyModelFallbacks.length > 0 ||
|
||||
legacyImageModelFallbacks.length > 0 ||
|
||||
Object.keys(legacyAliases).length > 0;
|
||||
if (!hasLegacy) return;
|
||||
|
||||
const models =
|
||||
agent.models && typeof agent.models === "object"
|
||||
? (agent.models as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
const ensureModel = (rawKey?: string) => {
|
||||
const key = String(rawKey ?? "").trim();
|
||||
if (!key) return;
|
||||
if (!models[key]) models[key] = {};
|
||||
};
|
||||
|
||||
ensureModel(legacyModel);
|
||||
ensureModel(legacyImageModel);
|
||||
for (const key of legacyAllowed) ensureModel(key);
|
||||
for (const key of legacyModelFallbacks) ensureModel(key);
|
||||
for (const key of legacyImageModelFallbacks) ensureModel(key);
|
||||
for (const target of Object.values(legacyAliases)) {
|
||||
ensureModel(String(target ?? ""));
|
||||
}
|
||||
|
||||
for (const [alias, targetRaw] of Object.entries(legacyAliases)) {
|
||||
const target = String(targetRaw ?? "").trim();
|
||||
if (!target) continue;
|
||||
const entry =
|
||||
models[target] && typeof models[target] === "object"
|
||||
? (models[target] as Record<string, unknown>)
|
||||
: {};
|
||||
if (!("alias" in entry)) {
|
||||
entry.alias = alias;
|
||||
models[target] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
const currentModel =
|
||||
agent.model && typeof agent.model === "object"
|
||||
? (agent.model as Record<string, unknown>)
|
||||
: null;
|
||||
if (currentModel) {
|
||||
if (!currentModel.primary && legacyModel) {
|
||||
currentModel.primary = legacyModel;
|
||||
}
|
||||
if (
|
||||
legacyModelFallbacks.length > 0 &&
|
||||
(!Array.isArray(currentModel.fallbacks) ||
|
||||
currentModel.fallbacks.length === 0)
|
||||
) {
|
||||
currentModel.fallbacks = legacyModelFallbacks;
|
||||
}
|
||||
agent.model = currentModel;
|
||||
} else if (legacyModel || legacyModelFallbacks.length > 0) {
|
||||
agent.model = {
|
||||
primary: legacyModel,
|
||||
fallbacks: legacyModelFallbacks.length ? legacyModelFallbacks : [],
|
||||
};
|
||||
}
|
||||
|
||||
const currentImageModel =
|
||||
agent.imageModel && typeof agent.imageModel === "object"
|
||||
? (agent.imageModel as Record<string, unknown>)
|
||||
: null;
|
||||
if (currentImageModel) {
|
||||
if (!currentImageModel.primary && legacyImageModel) {
|
||||
currentImageModel.primary = legacyImageModel;
|
||||
}
|
||||
if (
|
||||
legacyImageModelFallbacks.length > 0 &&
|
||||
(!Array.isArray(currentImageModel.fallbacks) ||
|
||||
currentImageModel.fallbacks.length === 0)
|
||||
) {
|
||||
currentImageModel.fallbacks = legacyImageModelFallbacks;
|
||||
}
|
||||
agent.imageModel = currentImageModel;
|
||||
} else if (legacyImageModel || legacyImageModelFallbacks.length > 0) {
|
||||
agent.imageModel = {
|
||||
primary: legacyImageModel,
|
||||
fallbacks: legacyImageModelFallbacks.length
|
||||
? legacyImageModelFallbacks
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
agent.models = models;
|
||||
|
||||
if (legacyModel !== undefined) {
|
||||
changes.push("Migrated agent.model string → agent.model.primary.");
|
||||
}
|
||||
if (legacyModelFallbacks.length > 0) {
|
||||
changes.push("Migrated agent.modelFallbacks → agent.model.fallbacks.");
|
||||
}
|
||||
if (legacyImageModel !== undefined) {
|
||||
changes.push(
|
||||
"Migrated agent.imageModel string → agent.imageModel.primary.",
|
||||
);
|
||||
}
|
||||
if (legacyImageModelFallbacks.length > 0) {
|
||||
changes.push(
|
||||
"Migrated agent.imageModelFallbacks → agent.imageModel.fallbacks.",
|
||||
);
|
||||
}
|
||||
if (legacyAllowed.length > 0) {
|
||||
changes.push("Migrated agent.allowedModels → agent.models.");
|
||||
}
|
||||
if (Object.keys(legacyAliases).length > 0) {
|
||||
changes.push("Migrated agent.modelAliases → agent.models.*.alias.");
|
||||
}
|
||||
|
||||
delete agent.allowedModels;
|
||||
delete agent.modelAliases;
|
||||
delete agent.modelFallbacks;
|
||||
delete agent.imageModelFallbacks;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function findLegacyConfigIssues(raw: unknown): LegacyConfigIssue[] {
|
||||
@@ -180,7 +365,7 @@ export function findLegacyConfigIssues(raw: unknown): LegacyConfigIssue[] {
|
||||
}
|
||||
cursor = (cursor as Record<string, unknown>)[key];
|
||||
}
|
||||
if (cursor !== undefined) {
|
||||
if (cursor !== undefined && (!rule.match || rule.match(cursor, root))) {
|
||||
issues.push({ path: rule.path.join("."), message: rule.message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,90 +1,56 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyLoggingDefaults, applyModelAliasDefaults } from "./defaults.js";
|
||||
import { applyModelDefaults } from "./defaults.js";
|
||||
import type { ClawdbotConfig } from "./types.js";
|
||||
|
||||
describe("applyModelAliasDefaults", () => {
|
||||
it("adds default shorthands", () => {
|
||||
const cfg = { agent: {} } satisfies ClawdbotConfig;
|
||||
const next = applyModelAliasDefaults(cfg);
|
||||
describe("applyModelDefaults", () => {
|
||||
it("adds default aliases when models are present", () => {
|
||||
const cfg = {
|
||||
agent: {
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-5.2": {},
|
||||
},
|
||||
},
|
||||
} satisfies ClawdbotConfig;
|
||||
const next = applyModelDefaults(cfg);
|
||||
|
||||
expect(next.agent?.modelAliases).toEqual({
|
||||
opus: "anthropic/claude-opus-4-5",
|
||||
sonnet: "anthropic/claude-sonnet-4-5",
|
||||
gpt: "openai/gpt-5.2",
|
||||
"gpt-mini": "openai/gpt-5-mini",
|
||||
gemini: "google/gemini-3-pro-preview",
|
||||
"gemini-flash": "google/gemini-3-flash-preview",
|
||||
});
|
||||
expect(next.agent?.models?.["anthropic/claude-opus-4-5"]?.alias).toBe(
|
||||
"opus",
|
||||
);
|
||||
expect(next.agent?.models?.["openai/gpt-5.2"]?.alias).toBe("gpt");
|
||||
});
|
||||
|
||||
it("normalizes casing when alias matches the default target", () => {
|
||||
it("does not override existing aliases", () => {
|
||||
const cfg = {
|
||||
agent: { modelAliases: { Opus: "anthropic/claude-opus-4-5" } },
|
||||
agent: {
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
} satisfies ClawdbotConfig;
|
||||
|
||||
const next = applyModelAliasDefaults(cfg);
|
||||
const next = applyModelDefaults(cfg);
|
||||
|
||||
expect(next.agent?.modelAliases).toMatchObject({
|
||||
opus: "anthropic/claude-opus-4-5",
|
||||
});
|
||||
expect(next.agent?.modelAliases).not.toHaveProperty("Opus");
|
||||
expect(next.agent?.models?.["anthropic/claude-opus-4-5"]?.alias).toBe(
|
||||
"Opus",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not override existing alias values", () => {
|
||||
it("respects explicit empty alias disables", () => {
|
||||
const cfg = {
|
||||
agent: { modelAliases: { gpt: "openai/gpt-4.1" } },
|
||||
agent: {
|
||||
models: {
|
||||
"google/gemini-3-pro-preview": { alias: "" },
|
||||
"google/gemini-3-flash-preview": {},
|
||||
},
|
||||
},
|
||||
} satisfies ClawdbotConfig;
|
||||
|
||||
const next = applyModelAliasDefaults(cfg);
|
||||
const next = applyModelDefaults(cfg);
|
||||
|
||||
expect(next.agent?.modelAliases?.gpt).toBe("openai/gpt-4.1");
|
||||
expect(next.agent?.modelAliases).toMatchObject({
|
||||
"gpt-mini": "openai/gpt-5-mini",
|
||||
opus: "anthropic/claude-opus-4-5",
|
||||
sonnet: "anthropic/claude-sonnet-4-5",
|
||||
gemini: "google/gemini-3-pro-preview",
|
||||
"gemini-flash": "google/gemini-3-flash-preview",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not rename when casing differs and value differs", () => {
|
||||
const cfg = {
|
||||
agent: { modelAliases: { GPT: "openai/gpt-4.1-mini" } },
|
||||
} satisfies ClawdbotConfig;
|
||||
|
||||
const next = applyModelAliasDefaults(cfg);
|
||||
|
||||
expect(next.agent?.modelAliases).toMatchObject({
|
||||
GPT: "openai/gpt-4.1-mini",
|
||||
});
|
||||
expect(next.agent?.modelAliases).not.toHaveProperty("gpt");
|
||||
});
|
||||
|
||||
it("respects explicit empty-string disables", () => {
|
||||
const cfg = {
|
||||
agent: { modelAliases: { gemini: "" } },
|
||||
} satisfies ClawdbotConfig;
|
||||
|
||||
const next = applyModelAliasDefaults(cfg);
|
||||
|
||||
expect(next.agent?.modelAliases?.gemini).toBe("");
|
||||
expect(next.agent?.modelAliases).toHaveProperty(
|
||||
expect(next.agent?.models?.["google/gemini-3-pro-preview"]?.alias).toBe("");
|
||||
expect(next.agent?.models?.["google/gemini-3-flash-preview"]?.alias).toBe(
|
||||
"gemini-flash",
|
||||
"google/gemini-3-flash-preview",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyLoggingDefaults", () => {
|
||||
it("defaults redactSensitive to tools", () => {
|
||||
const result = applyLoggingDefaults({ logging: {} });
|
||||
expect(result.logging?.redactSensitive).toBe("tools");
|
||||
});
|
||||
|
||||
it("preserves explicit redactSensitive", () => {
|
||||
const result = applyLoggingDefaults({
|
||||
logging: { redactSensitive: "off" },
|
||||
});
|
||||
expect(result.logging?.redactSensitive).toBe("off");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import type { ClawdbotConfig } from "./types.js";
|
||||
|
||||
/**
|
||||
@@ -33,6 +32,15 @@ export function resolveStateDir(
|
||||
return path.join(homedir(), ".clawdbot");
|
||||
}
|
||||
|
||||
function resolveUserPath(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
if (trimmed.startsWith("~")) {
|
||||
return path.resolve(trimmed.replace("~", os.homedir()));
|
||||
}
|
||||
return path.resolve(trimmed);
|
||||
}
|
||||
|
||||
export const STATE_DIR_CLAWDBOT = resolveStateDir();
|
||||
|
||||
/**
|
||||
|
||||
@@ -87,10 +87,13 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"gateway.reload.mode": "Config Reload Mode",
|
||||
"gateway.reload.debounceMs": "Config Reload Debounce (ms)",
|
||||
"agent.workspace": "Workspace",
|
||||
"agent.model": "Default Model",
|
||||
"agent.imageModel": "Image Model",
|
||||
"agent.modelFallbacks": "Model Fallbacks",
|
||||
"agent.imageModelFallbacks": "Image Model Fallbacks",
|
||||
"auth.profiles": "Auth Profiles",
|
||||
"auth.order": "Auth Profile Order",
|
||||
"agent.models": "Models",
|
||||
"agent.model.primary": "Primary Model",
|
||||
"agent.model.fallbacks": "Model Fallbacks",
|
||||
"agent.imageModel.primary": "Image Model",
|
||||
"agent.imageModel.fallbacks": "Image Model Fallbacks",
|
||||
"ui.seamColor": "Accent Color",
|
||||
"browser.controlUrl": "Browser Control URL",
|
||||
"session.agentToAgent.maxPingPongTurns": "Agent-to-Agent Ping-Pong Turns",
|
||||
@@ -114,12 +117,18 @@ const FIELD_HELP: Record<string, string> = {
|
||||
'Hot reload strategy for config changes ("hybrid" recommended).',
|
||||
"gateway.reload.debounceMs":
|
||||
"Debounce window (ms) before applying config changes.",
|
||||
"agent.modelFallbacks":
|
||||
"auth.profiles": "Named auth profiles (provider + mode + optional email).",
|
||||
"auth.order":
|
||||
"Ordered auth profile IDs per provider (used for automatic failover).",
|
||||
"agent.models":
|
||||
"Configured model catalog (keys are full provider/model IDs).",
|
||||
"agent.model.primary": "Primary model (provider/model).",
|
||||
"agent.model.fallbacks":
|
||||
"Ordered fallback models (provider/model). Used when the primary model fails.",
|
||||
"agent.imageModel":
|
||||
"Optional image-capable model (provider/model) used by the image tool.",
|
||||
"agent.imageModelFallbacks":
|
||||
"Ordered fallback image models (provider/model) used by the image tool.",
|
||||
"agent.imageModel.primary":
|
||||
"Optional image model (provider/model) used when the primary model lacks image input.",
|
||||
"agent.imageModel.fallbacks":
|
||||
"Ordered fallback image models (provider/model).",
|
||||
"session.agentToAgent.maxPingPongTurns":
|
||||
"Max reply-back turns between requester and target (0–5).",
|
||||
};
|
||||
|
||||
@@ -34,6 +34,7 @@ export type SessionEntry = {
|
||||
elevatedLevel?: string;
|
||||
providerOverride?: string;
|
||||
modelOverride?: string;
|
||||
authProfileOverride?: string;
|
||||
groupActivation?: "mention" | "always";
|
||||
groupActivationNeedsSystemIntro?: boolean;
|
||||
sendPolicy?: "allow" | "deny";
|
||||
|
||||
@@ -639,7 +639,28 @@ export type ModelsConfig = {
|
||||
providers?: Record<string, ModelProviderConfig>;
|
||||
};
|
||||
|
||||
export type AuthProfileConfig = {
|
||||
provider: string;
|
||||
mode: "api_key" | "oauth";
|
||||
email?: string;
|
||||
};
|
||||
|
||||
export type AuthConfig = {
|
||||
profiles?: Record<string, AuthProfileConfig>;
|
||||
order?: Record<string, string[]>;
|
||||
};
|
||||
|
||||
export type AgentModelEntryConfig = {
|
||||
alias?: string;
|
||||
};
|
||||
|
||||
export type AgentModelListConfig = {
|
||||
primary?: string;
|
||||
fallbacks?: string[];
|
||||
};
|
||||
|
||||
export type ClawdbotConfig = {
|
||||
auth?: AuthConfig;
|
||||
env?: {
|
||||
/** Opt-in: import missing secrets from a login shell environment (exec `$SHELL -l -c 'env -0'`). */
|
||||
shellEnv?: {
|
||||
@@ -669,22 +690,16 @@ export type ClawdbotConfig = {
|
||||
skills?: SkillsConfig;
|
||||
models?: ModelsConfig;
|
||||
agent?: {
|
||||
/** Model id (provider/model), e.g. "anthropic/claude-opus-4-5". */
|
||||
model?: string;
|
||||
/** Optional image-capable model (provider/model) used by the image tool. */
|
||||
imageModel?: string;
|
||||
/** Primary model and fallbacks (provider/model). */
|
||||
model?: AgentModelListConfig;
|
||||
/** Optional image-capable model and fallbacks (provider/model). */
|
||||
imageModel?: AgentModelListConfig;
|
||||
/** Model catalog with optional aliases (full provider/model keys). */
|
||||
models?: Record<string, AgentModelEntryConfig>;
|
||||
/** Agent working directory (preferred). Used as the default cwd for agent runs. */
|
||||
workspace?: string;
|
||||
/** Optional IANA timezone for the user (used in system prompt; defaults to host timezone). */
|
||||
userTimezone?: string;
|
||||
/** Optional allowlist for /model (provider/model or model-only). */
|
||||
allowedModels?: string[];
|
||||
/** Optional model aliases for /model (alias -> provider/model). */
|
||||
modelAliases?: Record<string, string>;
|
||||
/** Ordered fallback models (provider/model). */
|
||||
modelFallbacks?: string[];
|
||||
/** Ordered fallback image models (provider/model) for the image tool. */
|
||||
imageModelFallbacks?: string[];
|
||||
/** Optional display-only context window override (used for % in status UIs). */
|
||||
contextTokens?: number;
|
||||
/** Default thinking level when no /think directive is present. */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
applyIdentityDefaults,
|
||||
applyModelAliasDefaults,
|
||||
applyModelDefaults,
|
||||
applySessionDefaults,
|
||||
} from "./defaults.js";
|
||||
import { findLegacyConfigIssues } from "./legacy.js";
|
||||
@@ -34,7 +34,7 @@ export function validateConfigObject(
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
config: applyModelAliasDefaults(
|
||||
config: applyModelDefaults(
|
||||
applySessionDefaults(
|
||||
applyIdentityDefaults(validated.data as ClawdbotConfig),
|
||||
),
|
||||
|
||||
@@ -373,17 +373,46 @@ export const ClawdbotSchema = z.object({
|
||||
seamColor: HexColorSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
auth: z
|
||||
.object({
|
||||
profiles: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
provider: z.string(),
|
||||
mode: z.union([z.literal("api_key"), z.literal("oauth")]),
|
||||
email: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
order: z.record(z.string(), z.array(z.string())).optional(),
|
||||
})
|
||||
.optional(),
|
||||
models: ModelsConfigSchema,
|
||||
agent: z
|
||||
.object({
|
||||
model: z.string().optional(),
|
||||
imageModel: z.string().optional(),
|
||||
model: z
|
||||
.object({
|
||||
primary: z.string().optional(),
|
||||
fallbacks: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
imageModel: z
|
||||
.object({
|
||||
primary: z.string().optional(),
|
||||
fallbacks: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
models: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
alias: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
workspace: z.string().optional(),
|
||||
userTimezone: z.string().optional(),
|
||||
allowedModels: z.array(z.string()).optional(),
|
||||
modelAliases: z.record(z.string(), z.string()).optional(),
|
||||
modelFallbacks: z.array(z.string()).optional(),
|
||||
imageModelFallbacks: z.array(z.string()).optional(),
|
||||
contextTokens: z.number().int().positive().optional(),
|
||||
tools: z
|
||||
.object({
|
||||
|
||||
Reference in New Issue
Block a user