diff --git a/CHANGELOG.md b/CHANGELOG.md index 22eda1075..c841f9130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Models: add image-specific model config (`agent.imageModel` + fallbacks) and scan support. - Agent tools: new `image` tool routed to the image model (when configured). - Config: default model shorthands (`opus`, `sonnet`, `gpt`, `gpt-mini`, `gemini`, `gemini-flash`). +- Docs: document built-in model shorthands + precedence (user config wins). ### Fixes - Android: tapping the foreground service notification brings the app to the front. (#179) — thanks @Syhids diff --git a/docs/configuration.md b/docs/configuration.md index 56ca4246f..99673c336 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -434,6 +434,17 @@ Controls the embedded agent runtime (model/thinking/verbose/timeouts). `imageModel` selects an image-capable model for the `image` tool. `imageModelFallbacks` lists ordered fallback image models for the `image` tool. +Clawdbot also ships a few built-in `modelAliases` shorthands (when an `agent` section exists): + +- `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` + +If you configure the same alias name (case-insensitive) yourself, your value wins (defaults never override). + ```json5 { agent: { diff --git a/docs/faq.md b/docs/faq.md index 2f2266ba2..c47d44e94 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -530,8 +530,15 @@ Use `/model` to switch without restarting: /model sonnet /model haiku /model opus +/model gpt +/model gpt-mini +/model gemini +/model gemini-flash ``` +Clawdbot ships a few default model shorthands (you can override them in config): +`opus`, `sonnet`, `gpt`, `gpt-mini`, `gemini`, `gemini-flash`. + **Setup:** Configure allowed models and aliases in `clawdbot.json`: ```json diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 22824d2df..cc36943cb 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -8,6 +8,7 @@ import { ensureAgentWorkspace, } from "../agents/workspace.js"; import { type ClawdbotConfig, CONFIG_PATH_CLAWDBOT } from "../config/config.js"; +import { applyModelAliasDefaults } from "../config/defaults.js"; import { resolveSessionTranscriptsDir } from "../config/sessions.js"; import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; @@ -30,7 +31,9 @@ async function readConfigFileRaw(): Promise<{ async function writeConfigFile(cfg: ClawdbotConfig) { await fs.mkdir(path.dirname(CONFIG_PATH_CLAWDBOT), { recursive: true }); - const json = JSON.stringify(cfg, null, 2).trimEnd().concat("\n"); + const json = JSON.stringify(applyModelAliasDefaults(cfg), null, 2) + .trimEnd() + .concat("\n"); await fs.writeFile(CONFIG_PATH_CLAWDBOT, json, "utf-8"); } diff --git a/src/config/defaults.ts b/src/config/defaults.ts index fcf52e021..725d71695 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -5,6 +5,20 @@ type WarnState = { warned: boolean }; let defaultWarnState: WarnState = { warned: false }; +const DEFAULT_MODEL_ALIASES: Readonly> = { + // Anthropic (pi-ai catalog uses "latest" ids without date suffix) + opus: "anthropic/claude-opus-4-5", + sonnet: "anthropic/claude-sonnet-4-5", + + // OpenAI + gpt: "openai/gpt-5.2", + "gpt-mini": "openai/gpt-5-mini", + + // Google Gemini (3.x are preview ids in the catalog) + gemini: "google/gemini-3-pro-preview", + "gemini-flash": "google/gemini-3-flash-preview", +}; + export type SessionDefaultsOptions = { warn?: (message: string) => void; warnState?: WarnState; @@ -78,6 +92,56 @@ export function applyTalkApiKey(config: ClawdbotConfig): ClawdbotConfig { }; } +function normalizeAliasKey(value: string): string { + return value.trim().toLowerCase(); +} + +export function applyModelAliasDefaults(cfg: ClawdbotConfig): ClawdbotConfig { + const existingAgent = cfg.agent; + if (!existingAgent) return cfg; + const existingAliases = existingAgent?.modelAliases ?? {}; + + const byNormalized = new Map(); + for (const key of Object.keys(existingAliases)) { + const norm = normalizeAliasKey(key); + if (!norm) continue; + if (!byNormalized.has(norm)) byNormalized.set(norm, key); + } + + let mutated = false; + const nextAliases: Record = { ...existingAliases }; + + 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; + } + } + + if (!mutated) return cfg; + + return { + ...cfg, + agent: { + ...existingAgent, + modelAliases: nextAliases, + }, + }; +} + export function resetSessionDefaultsWarningForTests() { defaultWarnState = { warned: false }; } diff --git a/src/config/model-alias-defaults.test.ts b/src/config/model-alias-defaults.test.ts new file mode 100644 index 000000000..b2599f94f --- /dev/null +++ b/src/config/model-alias-defaults.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from "vitest"; +import { applyModelAliasDefaults } from "./defaults.js"; +import type { ClawdbotConfig } from "./types.js"; + +describe("applyModelAliasDefaults", () => { + it("adds default shorthands", () => { + const cfg = { agent: {} } satisfies ClawdbotConfig; + const next = applyModelAliasDefaults(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", + }); + }); + + it("normalizes casing when alias matches the default target", () => { + const cfg = { + agent: { modelAliases: { Opus: "anthropic/claude-opus-4-5" } }, + } satisfies ClawdbotConfig; + + const next = applyModelAliasDefaults(cfg); + + expect(next.agent?.modelAliases).toMatchObject({ + opus: "anthropic/claude-opus-4-5", + }); + expect(next.agent?.modelAliases).not.toHaveProperty("Opus"); + }); + + it("does not override existing alias values", () => { + const cfg = { + agent: { modelAliases: { gpt: "openai/gpt-4.1" } }, + } satisfies ClawdbotConfig; + + const next = applyModelAliasDefaults(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( + "gemini-flash", + "google/gemini-3-flash-preview", + ); + }); +}); diff --git a/src/config/validation.ts b/src/config/validation.ts index e2d2cfb3c..ecf57d8ab 100644 --- a/src/config/validation.ts +++ b/src/config/validation.ts @@ -1,4 +1,8 @@ -import { applyIdentityDefaults, applySessionDefaults } from "./defaults.js"; +import { + applyIdentityDefaults, + applyModelAliasDefaults, + applySessionDefaults, +} from "./defaults.js"; import { findLegacyConfigIssues } from "./legacy.js"; import type { ClawdbotConfig, ConfigValidationIssue } from "./types.js"; import { ClawdbotSchema } from "./zod-schema.js"; @@ -30,8 +34,10 @@ export function validateConfigObject( } return { ok: true, - config: applySessionDefaults( - applyIdentityDefaults(validated.data as ClawdbotConfig), + config: applyModelAliasDefaults( + applySessionDefaults( + applyIdentityDefaults(validated.data as ClawdbotConfig), + ), ), }; }