diff --git a/CHANGELOG.md b/CHANGELOG.md index 075348c91..a9585be95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Docs: https://docs.clawd.bot - macOS: exec approvals now respect wildcard agent allowlists (`*`). - UI: remove the chat stop button and keep the composer aligned to the bottom edge. - Configure: restrict the model allowlist picker to OAuth-compatible Anthropic models and preselect Opus 4.5. +- Configure: seed model fallbacks from the allowlist selection when multiple models are chosen. +- Model picker: list the full catalog when no model allowlist is configured. ## 2026.1.20 diff --git a/src/auto-reply/reply.directive.directive-behavior.lists-allowlisted-models-model-list.test.ts b/src/auto-reply/reply.directive.directive-behavior.lists-allowlisted-models-model-list.test.ts index 0118ecfcc..ffac7deab 100644 --- a/src/auto-reply/reply.directive.directive-behavior.lists-allowlisted-models-model-list.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.lists-allowlisted-models-model-list.test.ts @@ -121,7 +121,7 @@ describe("directive behavior", () => { expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); - it("uses configured models when no allowlist is set", async () => { + it("includes catalog models when no allowlist is set", async () => { await withTempHome(async (home) => { vi.mocked(runEmbeddedPiAgent).mockReset(); vi.mocked(loadModelCatalog).mockResolvedValueOnce([ @@ -153,7 +153,7 @@ describe("directive behavior", () => { expect(text).toContain("anthropic/claude-opus-4-5"); expect(text).toContain("openai/gpt-4.1-mini"); expect(text).toContain("minimax/MiniMax-M2.1"); - expect(text).not.toContain("xai/grok-4"); + expect(text).toContain("xai/grok-4"); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); diff --git a/src/auto-reply/reply/directive-handling.model.ts b/src/auto-reply/reply/directive-handling.model.ts index f163c3ff4..3f4200222 100644 --- a/src/auto-reply/reply/directive-handling.model.ts +++ b/src/auto-reply/reply/directive-handling.model.ts @@ -101,6 +101,13 @@ function buildModelPickerCatalog(params: { const hasAllowlist = Object.keys(params.cfg.agents?.defaults?.models ?? {}).length > 0; if (!hasAllowlist) { + for (const entry of params.allowedModelCatalog) { + push({ + provider: entry.provider, + id: entry.id ?? "", + name: entry.name, + }); + } for (const entry of buildConfiguredCatalog()) { push(entry); } diff --git a/src/commands/configure.gateway-auth.ts b/src/commands/configure.gateway-auth.ts index ba09ac40e..ad9406195 100644 --- a/src/commands/configure.gateway-auth.ts +++ b/src/commands/configure.gateway-auth.ts @@ -6,6 +6,7 @@ import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-c import { promptAuthChoiceGrouped } from "./auth-choice-prompt.js"; import { applyModelAllowlist, + applyModelFallbacksFromSelection, applyPrimaryModel, promptDefaultModel, promptModelAllowlist, @@ -90,6 +91,7 @@ export async function promptAuthConfig( }); if (allowlistSelection.models) { next = applyModelAllowlist(next, allowlistSelection.models); + next = applyModelFallbacksFromSelection(next, allowlistSelection.models); } return next; diff --git a/src/commands/model-picker.test.ts b/src/commands/model-picker.test.ts index 9c4b2627a..84acd9f99 100644 --- a/src/commands/model-picker.test.ts +++ b/src/commands/model-picker.test.ts @@ -2,7 +2,12 @@ import { describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../config/config.js"; import { makePrompter } from "./onboarding/__tests__/test-utils.js"; -import { applyModelAllowlist, promptDefaultModel, promptModelAllowlist } from "./model-picker.js"; +import { + applyModelAllowlist, + applyModelFallbacksFromSelection, + promptDefaultModel, + promptModelAllowlist, +} from "./model-picker.js"; const loadModelCatalog = vi.hoisted(() => vi.fn()); vi.mock("../agents/model-catalog.js", () => ({ @@ -170,3 +175,40 @@ describe("applyModelAllowlist", () => { expect(next.agents?.defaults?.models).toBeUndefined(); }); }); + +describe("applyModelFallbacksFromSelection", () => { + it("sets fallbacks from selection when the primary is included", () => { + const config = { + agents: { + defaults: { + model: { primary: "anthropic/claude-opus-4-5" }, + }, + }, + } as ClawdbotConfig; + + const next = applyModelFallbacksFromSelection(config, [ + "anthropic/claude-opus-4-5", + "anthropic/claude-sonnet-4-5", + ]); + expect(next.agents?.defaults?.model).toEqual({ + primary: "anthropic/claude-opus-4-5", + fallbacks: ["anthropic/claude-sonnet-4-5"], + }); + }); + + it("keeps existing fallbacks when the primary is not selected", () => { + const config = { + agents: { + defaults: { + model: { primary: "anthropic/claude-opus-4-5", fallbacks: ["openai/gpt-5.2"] }, + }, + }, + } as ClawdbotConfig; + + const next = applyModelFallbacksFromSelection(config, ["openai/gpt-5.2"]); + expect(next.agents?.defaults?.model).toEqual({ + primary: "anthropic/claude-opus-4-5", + fallbacks: ["openai/gpt-5.2"], + }); + }); +}); diff --git a/src/commands/model-picker.ts b/src/commands/model-picker.ts index cd826b884..5cceb1e94 100644 --- a/src/commands/model-picker.ts +++ b/src/commands/model-picker.ts @@ -446,3 +446,44 @@ export function applyModelAllowlist(cfg: ClawdbotConfig, models: string[]): Claw }, }; } + +export function applyModelFallbacksFromSelection( + cfg: ClawdbotConfig, + selection: string[], +): ClawdbotConfig { + const normalized = normalizeModelKeys(selection); + if (normalized.length <= 1) return cfg; + + const resolved = resolveConfiguredModelRef({ + cfg, + defaultProvider: DEFAULT_PROVIDER, + defaultModel: DEFAULT_MODEL, + }); + const resolvedKey = modelKey(resolved.provider, resolved.model); + if (!normalized.includes(resolvedKey)) return cfg; + + const defaults = cfg.agents?.defaults; + const existingModel = defaults?.model; + const existingPrimary = + typeof existingModel === "string" + ? existingModel + : existingModel && typeof existingModel === "object" + ? existingModel.primary + : undefined; + + const fallbacks = normalized.filter((key) => key !== resolvedKey); + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...defaults, + model: { + ...(typeof existingModel === "object" ? existingModel : undefined), + primary: existingPrimary ?? resolvedKey, + fallbacks, + }, + }, + }, + }; +}