fix: allow default model outside allowlist

This commit is contained in:
Peter Steinberger
2026-01-09 04:18:21 +00:00
parent 7e4d5c9f84
commit 64fc5fa9fc
9 changed files with 255 additions and 127 deletions

View File

@@ -1,146 +1,56 @@
import { describe, expect, it } from "vitest";
import type { ClawdbotConfig } from "../config/config.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
import {
normalizeProviderId,
resolveConfiguredModelRef,
} from "./model-selection.js";
import { buildAllowedModelSet, modelKey } from "./model-selection.js";
describe("resolveConfiguredModelRef", () => {
it("parses provider/model from agent.model.primary", () => {
const cfg = {
agent: { model: { primary: "openai/gpt-4.1-mini" } },
} satisfies ClawdbotConfig;
const catalog = [
{
provider: "openai",
id: "gpt-4",
name: "GPT-4",
},
];
const resolved = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
expect(resolved).toEqual({ provider: "openai", model: "gpt-4.1-mini" });
});
it("falls back to anthropic when agent.model.primary omits provider", () => {
const cfg = {
agent: { model: { primary: "claude-opus-4-5" } },
} satisfies ClawdbotConfig;
const resolved = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
expect(resolved).toEqual({
provider: "anthropic",
model: "claude-opus-4-5",
});
});
it("falls back to defaults when agent.model is missing", () => {
const cfg = {} satisfies ClawdbotConfig;
const resolved = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
expect(resolved).toEqual({
provider: DEFAULT_PROVIDER,
model: DEFAULT_MODEL,
});
});
it("resolves agent.model aliases when configured", () => {
describe("buildAllowedModelSet", () => {
it("always allows the configured default model", () => {
const cfg = {
agent: {
model: { primary: "Opus" },
models: {
"anthropic/claude-opus-4-5": { alias: "Opus" },
"openai/gpt-4": { alias: "gpt4" },
},
},
} satisfies ClawdbotConfig;
} as ClawdbotConfig;
const resolved = resolveConfiguredModelRef({
const allowed = buildAllowedModelSet({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
catalog,
defaultProvider: "claude-cli",
defaultModel: "opus-4.5",
});
expect(resolved).toEqual({
provider: "anthropic",
model: "claude-opus-4-5",
});
expect(allowed.allowAny).toBe(false);
expect(allowed.allowedKeys.has(modelKey("openai", "gpt-4"))).toBe(true);
expect(
allowed.allowedKeys.has(modelKey("claude-cli", "opus-4.5")),
).toBe(true);
});
it("normalizes z.ai provider in agent.model", () => {
it("includes the default model when no allowlist is set", () => {
const cfg = {
agent: { model: "z.ai/glm-4.7" },
} satisfies ClawdbotConfig;
agent: {},
} as ClawdbotConfig;
const resolved = resolveConfiguredModelRef({
const allowed = buildAllowedModelSet({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
catalog,
defaultProvider: "claude-cli",
defaultModel: "opus-4.5",
});
expect(resolved).toEqual({ provider: "zai", model: "glm-4.7" });
});
it("normalizes z-ai provider in agent.model", () => {
const cfg = {
agent: { model: "z-ai/glm-4.7" },
} satisfies ClawdbotConfig;
const resolved = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
expect(resolved).toEqual({ provider: "zai", model: "glm-4.7" });
});
it("normalizes provider casing in agent.model", () => {
const cfg = {
agent: { model: "OpenAI/gpt-4.1-mini" },
} satisfies ClawdbotConfig;
const resolved = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
expect(resolved).toEqual({ provider: "openai", model: "gpt-4.1-mini" });
});
it("normalizes z.ai casing in agent.model", () => {
const cfg = {
agent: { model: "Z.AI/glm-4.7" },
} satisfies ClawdbotConfig;
const resolved = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
expect(resolved).toEqual({ provider: "zai", model: "glm-4.7" });
});
});
describe("normalizeProviderId", () => {
it("normalizes z.ai aliases to canonical zai", () => {
expect(normalizeProviderId("z.ai")).toBe("zai");
expect(normalizeProviderId("z-ai")).toBe("zai");
});
it("normalizes provider casing", () => {
expect(normalizeProviderId("OpenAI")).toBe("openai");
expect(normalizeProviderId("Z.AI")).toBe("zai");
expect(allowed.allowAny).toBe(true);
expect(allowed.allowedKeys.has(modelKey("openai", "gpt-4"))).toBe(true);
expect(
allowed.allowedKeys.has(modelKey("claude-cli", "opus-4.5")),
).toBe(true);
});
});

View File

@@ -124,6 +124,7 @@ export function buildAllowedModelSet(params: {
cfg: ClawdbotConfig;
catalog: ModelCatalogEntry[];
defaultProvider: string;
defaultModel?: string;
}): {
allowAny: boolean;
allowedCatalog: ModelCatalogEntry[];
@@ -134,11 +135,17 @@ export function buildAllowedModelSet(params: {
return Object.keys(modelMap);
})();
const allowAny = rawAllowlist.length === 0;
const defaultModel = params.defaultModel?.trim();
const defaultKey =
defaultModel && params.defaultProvider
? modelKey(params.defaultProvider, defaultModel)
: undefined;
const catalogKeys = new Set(
params.catalog.map((entry) => modelKey(entry.provider, entry.id)),
);
if (allowAny) {
if (defaultKey) catalogKeys.add(defaultKey);
return {
allowAny: true,
allowedCatalog: params.catalog,
@@ -156,11 +163,16 @@ export function buildAllowedModelSet(params: {
}
}
if (defaultKey) {
allowedKeys.add(defaultKey);
}
const allowedCatalog = params.catalog.filter((entry) =>
allowedKeys.has(modelKey(entry.provider, entry.id)),
);
if (allowedCatalog.length === 0) {
if (defaultKey) catalogKeys.add(defaultKey);
return {
allowAny: true,
allowedCatalog: params.catalog,