Skip to content

Commit 2899a98

Browse files
committed
feat(config): add default model shorthands
1 parent 7a63b49 commit 2899a98

7 files changed

Lines changed: 172 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Models: add image-specific model config (`agent.imageModel` + fallbacks) and scan support.
99
- Agent tools: new `image` tool routed to the image model (when configured).
1010
- Config: default model shorthands (`opus`, `sonnet`, `gpt`, `gpt-mini`, `gemini`, `gemini-flash`).
11+
- Docs: document built-in model shorthands + precedence (user config wins).
1112

1213
### Fixes
1314
- Android: tapping the foreground service notification brings the app to the front. (#179) — thanks @Syhids

docs/configuration.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,17 @@ Controls the embedded agent runtime (model/thinking/verbose/timeouts).
434434
`imageModel` selects an image-capable model for the `image` tool.
435435
`imageModelFallbacks` lists ordered fallback image models for the `image` tool.
436436

437+
Clawdbot also ships a few built-in `modelAliases` shorthands (when an `agent` section exists):
438+
439+
- `opus` -> `anthropic/claude-opus-4-5`
440+
- `sonnet` -> `anthropic/claude-sonnet-4-5`
441+
- `gpt` -> `openai/gpt-5.2`
442+
- `gpt-mini` -> `openai/gpt-5-mini`
443+
- `gemini` -> `google/gemini-3-pro-preview`
444+
- `gemini-flash` -> `google/gemini-3-flash-preview`
445+
446+
If you configure the same alias name (case-insensitive) yourself, your value wins (defaults never override).
447+
437448
```json5
438449
{
439450
agent: {

docs/faq.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,8 +530,15 @@ Use `/model` to switch without restarting:
530530
/model sonnet
531531
/model haiku
532532
/model opus
533+
/model gpt
534+
/model gpt-mini
535+
/model gemini
536+
/model gemini-flash
533537
```
534538

539+
Clawdbot ships a few default model shorthands (you can override them in config):
540+
`opus`, `sonnet`, `gpt`, `gpt-mini`, `gemini`, `gemini-flash`.
541+
535542
**Setup:** Configure allowed models and aliases in `clawdbot.json`:
536543

537544
```json

src/commands/setup.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ensureAgentWorkspace,
99
} from "../agents/workspace.js";
1010
import { type ClawdbotConfig, CONFIG_PATH_CLAWDBOT } from "../config/config.js";
11+
import { applyModelAliasDefaults } from "../config/defaults.js";
1112
import { resolveSessionTranscriptsDir } from "../config/sessions.js";
1213
import type { RuntimeEnv } from "../runtime.js";
1314
import { defaultRuntime } from "../runtime.js";
@@ -30,7 +31,9 @@ async function readConfigFileRaw(): Promise<{
3031

3132
async function writeConfigFile(cfg: ClawdbotConfig) {
3233
await fs.mkdir(path.dirname(CONFIG_PATH_CLAWDBOT), { recursive: true });
33-
const json = JSON.stringify(cfg, null, 2).trimEnd().concat("\n");
34+
const json = JSON.stringify(applyModelAliasDefaults(cfg), null, 2)
35+
.trimEnd()
36+
.concat("\n");
3437
await fs.writeFile(CONFIG_PATH_CLAWDBOT, json, "utf-8");
3538
}
3639

src/config/defaults.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ type WarnState = { warned: boolean };
55

66
let defaultWarnState: WarnState = { warned: false };
77

8+
const DEFAULT_MODEL_ALIASES: Readonly<Record<string, string>> = {
9+
// Anthropic (pi-ai catalog uses "latest" ids without date suffix)
10+
opus: "anthropic/claude-opus-4-5",
11+
sonnet: "anthropic/claude-sonnet-4-5",
12+
13+
// OpenAI
14+
gpt: "openai/gpt-5.2",
15+
"gpt-mini": "openai/gpt-5-mini",
16+
17+
// Google Gemini (3.x are preview ids in the catalog)
18+
gemini: "google/gemini-3-pro-preview",
19+
"gemini-flash": "google/gemini-3-flash-preview",
20+
};
21+
822
export type SessionDefaultsOptions = {
923
warn?: (message: string) => void;
1024
warnState?: WarnState;
@@ -78,6 +92,56 @@ export function applyTalkApiKey(config: ClawdbotConfig): ClawdbotConfig {
7892
};
7993
}
8094

95+
function normalizeAliasKey(value: string): string {
96+
return value.trim().toLowerCase();
97+
}
98+
99+
export function applyModelAliasDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
100+
const existingAgent = cfg.agent;
101+
if (!existingAgent) return cfg;
102+
const existingAliases = existingAgent?.modelAliases ?? {};
103+
104+
const byNormalized = new Map<string, string>();
105+
for (const key of Object.keys(existingAliases)) {
106+
const norm = normalizeAliasKey(key);
107+
if (!norm) continue;
108+
if (!byNormalized.has(norm)) byNormalized.set(norm, key);
109+
}
110+
111+
let mutated = false;
112+
const nextAliases: Record<string, string> = { ...existingAliases };
113+
114+
for (const [canonicalKey, target] of Object.entries(DEFAULT_MODEL_ALIASES)) {
115+
const norm = normalizeAliasKey(canonicalKey);
116+
const existingKey = byNormalized.get(norm);
117+
118+
if (!existingKey) {
119+
nextAliases[canonicalKey] = target;
120+
byNormalized.set(norm, canonicalKey);
121+
mutated = true;
122+
continue;
123+
}
124+
125+
const existingValue = String(existingAliases[existingKey] ?? "");
126+
if (existingKey !== canonicalKey && existingValue === target) {
127+
delete nextAliases[existingKey];
128+
nextAliases[canonicalKey] = target;
129+
byNormalized.set(norm, canonicalKey);
130+
mutated = true;
131+
}
132+
}
133+
134+
if (!mutated) return cfg;
135+
136+
return {
137+
...cfg,
138+
agent: {
139+
...existingAgent,
140+
modelAliases: nextAliases,
141+
},
142+
};
143+
}
144+
81145
export function resetSessionDefaultsWarningForTests() {
82146
defaultWarnState = { warned: false };
83147
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { describe, expect, it } from "vitest";
2+
import { applyModelAliasDefaults } from "./defaults.js";
3+
import type { ClawdbotConfig } from "./types.js";
4+
5+
describe("applyModelAliasDefaults", () => {
6+
it("adds default shorthands", () => {
7+
const cfg = { agent: {} } satisfies ClawdbotConfig;
8+
const next = applyModelAliasDefaults(cfg);
9+
10+
expect(next.agent?.modelAliases).toEqual({
11+
opus: "anthropic/claude-opus-4-5",
12+
sonnet: "anthropic/claude-sonnet-4-5",
13+
gpt: "openai/gpt-5.2",
14+
"gpt-mini": "openai/gpt-5-mini",
15+
gemini: "google/gemini-3-pro-preview",
16+
"gemini-flash": "google/gemini-3-flash-preview",
17+
});
18+
});
19+
20+
it("normalizes casing when alias matches the default target", () => {
21+
const cfg = {
22+
agent: { modelAliases: { Opus: "anthropic/claude-opus-4-5" } },
23+
} satisfies ClawdbotConfig;
24+
25+
const next = applyModelAliasDefaults(cfg);
26+
27+
expect(next.agent?.modelAliases).toMatchObject({
28+
opus: "anthropic/claude-opus-4-5",
29+
});
30+
expect(next.agent?.modelAliases).not.toHaveProperty("Opus");
31+
});
32+
33+
it("does not override existing alias values", () => {
34+
const cfg = {
35+
agent: { modelAliases: { gpt: "openai/gpt-4.1" } },
36+
} satisfies ClawdbotConfig;
37+
38+
const next = applyModelAliasDefaults(cfg);
39+
40+
expect(next.agent?.modelAliases?.gpt).toBe("openai/gpt-4.1");
41+
expect(next.agent?.modelAliases).toMatchObject({
42+
"gpt-mini": "openai/gpt-5-mini",
43+
opus: "anthropic/claude-opus-4-5",
44+
sonnet: "anthropic/claude-sonnet-4-5",
45+
gemini: "google/gemini-3-pro-preview",
46+
"gemini-flash": "google/gemini-3-flash-preview",
47+
});
48+
});
49+
50+
it("does not rename when casing differs and value differs", () => {
51+
const cfg = {
52+
agent: { modelAliases: { GPT: "openai/gpt-4.1-mini" } },
53+
} satisfies ClawdbotConfig;
54+
55+
const next = applyModelAliasDefaults(cfg);
56+
57+
expect(next.agent?.modelAliases).toMatchObject({
58+
GPT: "openai/gpt-4.1-mini",
59+
});
60+
expect(next.agent?.modelAliases).not.toHaveProperty("gpt");
61+
});
62+
63+
it("respects explicit empty-string disables", () => {
64+
const cfg = {
65+
agent: { modelAliases: { gemini: "" } },
66+
} satisfies ClawdbotConfig;
67+
68+
const next = applyModelAliasDefaults(cfg);
69+
70+
expect(next.agent?.modelAliases?.gemini).toBe("");
71+
expect(next.agent?.modelAliases).toHaveProperty(
72+
"gemini-flash",
73+
"google/gemini-3-flash-preview",
74+
);
75+
});
76+
});

src/config/validation.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { applyIdentityDefaults, applySessionDefaults } from "./defaults.js";
1+
import {
2+
applyIdentityDefaults,
3+
applyModelAliasDefaults,
4+
applySessionDefaults,
5+
} from "./defaults.js";
26
import { findLegacyConfigIssues } from "./legacy.js";
37
import type { ClawdbotConfig, ConfigValidationIssue } from "./types.js";
48
import { ClawdbotSchema } from "./zod-schema.js";
@@ -30,8 +34,10 @@ export function validateConfigObject(
3034
}
3135
return {
3236
ok: true,
33-
config: applySessionDefaults(
34-
applyIdentityDefaults(validated.data as ClawdbotConfig),
37+
config: applyModelAliasDefaults(
38+
applySessionDefaults(
39+
applyIdentityDefaults(validated.data as ClawdbotConfig),
40+
),
3541
),
3642
};
3743
}

0 commit comments

Comments
 (0)