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 (`*`).
|
- macOS: exec approvals now respect wildcard agent allowlists (`*`).
|
||||||
- UI: remove the chat stop button and keep the composer aligned to the bottom edge.
|
- 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: 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
|
## 2026.1.20
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ describe("directive behavior", () => {
|
|||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
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) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
vi.mocked(loadModelCatalog).mockResolvedValueOnce([
|
vi.mocked(loadModelCatalog).mockResolvedValueOnce([
|
||||||
@@ -153,7 +153,7 @@ describe("directive behavior", () => {
|
|||||||
expect(text).toContain("anthropic/claude-opus-4-5");
|
expect(text).toContain("anthropic/claude-opus-4-5");
|
||||||
expect(text).toContain("openai/gpt-4.1-mini");
|
expect(text).toContain("openai/gpt-4.1-mini");
|
||||||
expect(text).toContain("minimax/MiniMax-M2.1");
|
expect(text).toContain("minimax/MiniMax-M2.1");
|
||||||
expect(text).not.toContain("xai/grok-4");
|
expect(text).toContain("xai/grok-4");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -101,6 +101,13 @@ function buildModelPickerCatalog(params: {
|
|||||||
|
|
||||||
const hasAllowlist = Object.keys(params.cfg.agents?.defaults?.models ?? {}).length > 0;
|
const hasAllowlist = Object.keys(params.cfg.agents?.defaults?.models ?? {}).length > 0;
|
||||||
if (!hasAllowlist) {
|
if (!hasAllowlist) {
|
||||||
|
for (const entry of params.allowedModelCatalog) {
|
||||||
|
push({
|
||||||
|
provider: entry.provider,
|
||||||
|
id: entry.id ?? "",
|
||||||
|
name: entry.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
for (const entry of buildConfiguredCatalog()) {
|
for (const entry of buildConfiguredCatalog()) {
|
||||||
push(entry);
|
push(entry);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-c
|
|||||||
import { promptAuthChoiceGrouped } from "./auth-choice-prompt.js";
|
import { promptAuthChoiceGrouped } from "./auth-choice-prompt.js";
|
||||||
import {
|
import {
|
||||||
applyModelAllowlist,
|
applyModelAllowlist,
|
||||||
|
applyModelFallbacksFromSelection,
|
||||||
applyPrimaryModel,
|
applyPrimaryModel,
|
||||||
promptDefaultModel,
|
promptDefaultModel,
|
||||||
promptModelAllowlist,
|
promptModelAllowlist,
|
||||||
@@ -90,6 +91,7 @@ export async function promptAuthConfig(
|
|||||||
});
|
});
|
||||||
if (allowlistSelection.models) {
|
if (allowlistSelection.models) {
|
||||||
next = applyModelAllowlist(next, allowlistSelection.models);
|
next = applyModelAllowlist(next, allowlistSelection.models);
|
||||||
|
next = applyModelFallbacksFromSelection(next, allowlistSelection.models);
|
||||||
}
|
}
|
||||||
|
|
||||||
return next;
|
return next;
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { describe, expect, it, vi } from "vitest";
|
|||||||
|
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { makePrompter } from "./onboarding/__tests__/test-utils.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());
|
const loadModelCatalog = vi.hoisted(() => vi.fn());
|
||||||
vi.mock("../agents/model-catalog.js", () => ({
|
vi.mock("../agents/model-catalog.js", () => ({
|
||||||
@@ -170,3 +175,40 @@ describe("applyModelAllowlist", () => {
|
|||||||
expect(next.agents?.defaults?.models).toBeUndefined();
|
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