fix: model picker allowlist fallbacks

This commit is contained in:
Peter Steinberger
2026-01-21 11:22:23 +00:00
parent 884211a924
commit fb164b321e
6 changed files with 97 additions and 3 deletions

View File

@@ -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

View File

@@ -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();
});
});

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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"],
});
});
});

View File

@@ -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,
},
},
},
};
}