fix: model picker allowlist fallbacks
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user