refactor: centralize model allowlist normalization (#9898) (thanks @gumadeiras)

This commit is contained in:
Gustavo Madeira Santana
2026-02-05 16:33:17 -05:00
parent 678ae093a1
commit 5d8f729538
7 changed files with 125 additions and 53 deletions

View File

@@ -13,9 +13,9 @@ import {
isTimeoutError,
} from "./failover-error.js";
import {
buildConfiguredAllowlistKeys,
buildModelAliasIndex,
modelKey,
parseModelRef,
resolveConfiguredModelRef,
resolveModelRefFromString,
} from "./model-selection.js";
@@ -51,28 +51,6 @@ function shouldRethrowAbort(err: unknown): boolean {
return isAbortError(err) && !isTimeoutError(err);
}
function buildAllowedModelKeys(
cfg: OpenClawConfig | undefined,
defaultProvider: string,
): Set<string> | null {
const rawAllowlist = (() => {
const modelMap = cfg?.agents?.defaults?.models ?? {};
return Object.keys(modelMap);
})();
if (rawAllowlist.length === 0) {
return null;
}
const keys = new Set<string>();
for (const raw of rawAllowlist) {
const parsed = parseModelRef(String(raw ?? ""), defaultProvider);
if (!parsed) {
continue;
}
keys.add(modelKey(parsed.provider, parsed.model));
}
return keys.size > 0 ? keys : null;
}
function resolveImageFallbackCandidates(params: {
cfg: OpenClawConfig | undefined;
defaultProvider: string;
@@ -82,7 +60,10 @@ function resolveImageFallbackCandidates(params: {
cfg: params.cfg ?? {},
defaultProvider: params.defaultProvider,
});
const allowlist = buildAllowedModelKeys(params.cfg, params.defaultProvider);
const allowlist = buildConfiguredAllowlistKeys({
cfg: params.cfg,
defaultProvider: params.defaultProvider,
});
const seen = new Set<string>();
const candidates: ModelCandidate[] = [];
@@ -166,7 +147,10 @@ function resolveFallbackCandidates(params: {
cfg: params.cfg ?? {},
defaultProvider,
});
const allowlist = buildAllowedModelKeys(params.cfg, defaultProvider);
const allowlist = buildConfiguredAllowlistKeys({
cfg: params.cfg,
defaultProvider,
});
const seen = new Set<string>();
const candidates: ModelCandidate[] = [];

View File

@@ -29,6 +29,17 @@ describe("model-selection", () => {
});
});
it("normalizes anthropic alias refs to canonical model ids", () => {
expect(parseModelRef("anthropic/opus-4.6", "openai")).toEqual({
provider: "anthropic",
model: "claude-opus-4-6",
});
expect(parseModelRef("opus-4.6", "anthropic")).toEqual({
provider: "anthropic",
model: "claude-opus-4-6",
});
});
it("should use default provider if none specified", () => {
expect(parseModelRef("claude-3-5-sonnet", "anthropic")).toEqual({
provider: "anthropic",

View File

@@ -16,6 +16,12 @@ export type ModelAliasIndex = {
byKey: Map<string, string[]>;
};
const ANTHROPIC_MODEL_ALIASES: Record<string, string> = {
"opus-4.6": "claude-opus-4-6",
"opus-4.5": "claude-opus-4-5",
"sonnet-4.5": "claude-sonnet-4-5",
};
function normalizeAliasKey(value: string): string {
return value.trim().toLowerCase();
}
@@ -59,16 +65,7 @@ function normalizeAnthropicModelId(model: string): string {
return trimmed;
}
const lower = trimmed.toLowerCase();
if (lower === "opus-4.6") {
return "claude-opus-4-6";
}
if (lower === "opus-4.5") {
return "claude-opus-4-5";
}
if (lower === "sonnet-4.5") {
return "claude-sonnet-4-5";
}
return trimmed;
return ANTHROPIC_MODEL_ALIASES[lower] ?? trimmed;
}
function normalizeProviderModelId(provider: string, model: string): string {
@@ -102,6 +99,33 @@ export function parseModelRef(raw: string, defaultProvider: string): ModelRef |
return { provider, model: normalizedModel };
}
export function resolveAllowlistModelKey(raw: string, defaultProvider: string): string | null {
const parsed = parseModelRef(raw, defaultProvider);
if (!parsed) {
return null;
}
return modelKey(parsed.provider, parsed.model);
}
export function buildConfiguredAllowlistKeys(params: {
cfg: OpenClawConfig | undefined;
defaultProvider: string;
}): Set<string> | null {
const rawAllowlist = Object.keys(params.cfg?.agents?.defaults?.models ?? {});
if (rawAllowlist.length === 0) {
return null;
}
const keys = new Set<string>();
for (const raw of rawAllowlist) {
const key = resolveAllowlistModelKey(String(raw ?? ""), params.defaultProvider);
if (key) {
keys.add(key);
}
}
return keys.size > 0 ? keys : null;
}
export function buildModelAliasIndex(params: {
cfg: OpenClawConfig;
defaultProvider: string;

View File

@@ -36,6 +36,22 @@ describe("applyDefaultModelChoice", () => {
expect(applied.config.agents?.defaults?.models?.[defaultModel]).toEqual({});
});
it("adds canonical allowlist key for anthropic aliases", async () => {
const defaultModel = "anthropic/opus-4.6";
const applied = await applyDefaultModelChoice({
config: {},
setDefaultModel: false,
defaultModel,
applyProviderConfig: (config: OpenClawConfig) => config,
applyDefaultConfig: (config: OpenClawConfig) => config,
noteAgentModel: async () => {},
prompter: makePrompter(),
});
expect(applied.config.agents?.defaults?.models?.[defaultModel]).toEqual({});
expect(applied.config.agents?.defaults?.models?.["anthropic/claude-opus-4-6"]).toEqual({});
});
it("uses applyDefaultConfig path when setDefaultModel is true", async () => {
const defaultModel = "openai/gpt-5.1-codex";
const applied = await applyDefaultModelChoice({

View File

@@ -1,5 +1,6 @@
import type { OpenClawConfig } from "../config/config.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import { ensureModelAllowlistEntry } from "./model-allowlist.js";
export async function applyDefaultModelChoice(params: {
config: OpenClawConfig;
@@ -20,20 +21,10 @@ export async function applyDefaultModelChoice(params: {
}
const next = params.applyProviderConfig(params.config);
const models = { ...next.agents?.defaults?.models };
models[params.defaultModel] = {
...models[params.defaultModel],
};
const nextWithModel = {
...next,
agents: {
...next.agents,
defaults: {
...next.agents?.defaults,
models,
},
},
};
const nextWithModel = ensureModelAllowlistEntry({
cfg: next,
modelRef: params.defaultModel,
});
await params.noteAgentModel(params.defaultModel);
return { config: nextWithModel, agentModelOverride: params.defaultModel };
}

View File

@@ -0,0 +1,41 @@
import type { OpenClawConfig } from "../config/config.js";
import { DEFAULT_PROVIDER } from "../agents/defaults.js";
import { resolveAllowlistModelKey } from "../agents/model-selection.js";
export function ensureModelAllowlistEntry(params: {
cfg: OpenClawConfig;
modelRef: string;
defaultProvider?: string;
}): OpenClawConfig {
const rawModelRef = params.modelRef.trim();
if (!rawModelRef) {
return params.cfg;
}
const models = { ...params.cfg.agents?.defaults?.models };
const keySet = new Set<string>([rawModelRef]);
const canonicalKey = resolveAllowlistModelKey(
rawModelRef,
params.defaultProvider ?? DEFAULT_PROVIDER,
);
if (canonicalKey) {
keySet.add(canonicalKey);
}
for (const key of keySet) {
models[key] = {
...models[key],
};
}
return {
...params.cfg,
agents: {
...params.cfg.agents,
defaults: {
...params.cfg.agents?.defaults,
models,
},
},
};
}

View File

@@ -1,20 +1,25 @@
import type { OpenClawConfig } from "../config/config.js";
import { ensureModelAllowlistEntry } from "./model-allowlist.js";
export const OPENAI_DEFAULT_MODEL = "openai/gpt-5.1-codex";
export function applyOpenAIProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
const models = { ...cfg.agents?.defaults?.models };
const next = ensureModelAllowlistEntry({
cfg,
modelRef: OPENAI_DEFAULT_MODEL,
});
const models = { ...next.agents?.defaults?.models };
models[OPENAI_DEFAULT_MODEL] = {
...models[OPENAI_DEFAULT_MODEL],
alias: models[OPENAI_DEFAULT_MODEL]?.alias ?? "GPT",
};
return {
...cfg,
...next,
agents: {
...cfg.agents,
...next.agents,
defaults: {
...cfg.agents?.defaults,
...next.agents?.defaults,
models,
},
},