fix(agents): fall back to agents.defaults.model when agent has no model config (#24210)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 0f272b1027
Co-authored-by: bianbiandashen <16240681+bianbiandashen@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
边黎安
2026-02-23 16:18:55 +08:00
committed by GitHub
parent db32677f1d
commit a4c373935f
39 changed files with 434 additions and 251 deletions

View File

@@ -4,6 +4,8 @@ import type { OpenClawConfig } from "../config/config.js";
import {
resolveAgentConfig,
resolveAgentDir,
resolveAgentEffectiveModelPrimary,
resolveAgentExplicitModelPrimary,
resolveEffectiveModelFallbacks,
resolveAgentModelFallbacksOverride,
resolveAgentModelPrimary,
@@ -59,6 +61,43 @@ describe("resolveAgentConfig", () => {
});
});
it("resolves explicit and effective model primary separately", () => {
const cfgWithStringDefault = {
agents: {
defaults: {
model: "anthropic/claude-sonnet-4",
},
list: [{ id: "main" }],
},
} as unknown as OpenClawConfig;
expect(resolveAgentExplicitModelPrimary(cfgWithStringDefault, "main")).toBeUndefined();
expect(resolveAgentEffectiveModelPrimary(cfgWithStringDefault, "main")).toBe(
"anthropic/claude-sonnet-4",
);
const cfgWithObjectDefault: OpenClawConfig = {
agents: {
defaults: {
model: {
primary: "openai/gpt-5.2",
fallbacks: ["anthropic/claude-sonnet-4"],
},
},
list: [{ id: "main" }],
},
};
expect(resolveAgentExplicitModelPrimary(cfgWithObjectDefault, "main")).toBeUndefined();
expect(resolveAgentEffectiveModelPrimary(cfgWithObjectDefault, "main")).toBe("openai/gpt-5.2");
const cfgNoDefaults: OpenClawConfig = {
agents: {
list: [{ id: "main" }],
},
};
expect(resolveAgentExplicitModelPrimary(cfgNoDefaults, "main")).toBeUndefined();
expect(resolveAgentEffectiveModelPrimary(cfgNoDefaults, "main")).toBeUndefined();
});
it("supports per-agent model primary+fallbacks", () => {
const cfg: OpenClawConfig = {
agents: {
@@ -81,6 +120,8 @@ describe("resolveAgentConfig", () => {
};
expect(resolveAgentModelPrimary(cfg, "linus")).toBe("anthropic/claude-opus-4");
expect(resolveAgentExplicitModelPrimary(cfg, "linus")).toBe("anthropic/claude-opus-4");
expect(resolveAgentEffectiveModelPrimary(cfg, "linus")).toBe("anthropic/claude-opus-4");
expect(resolveAgentModelFallbacksOverride(cfg, "linus")).toEqual(["openai/gpt-5.2"]);
// If fallbacks isn't present, we don't override the global fallbacks.

View File

@@ -1,5 +1,6 @@
import path from "node:path";
import type { OpenClawConfig } from "../config/config.js";
import { resolveAgentModelFallbackValues } from "../config/model-input.js";
import { resolveStateDir } from "../config/paths.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import {
@@ -142,16 +143,43 @@ export function resolveAgentSkillsFilter(
return normalizeSkillFilter(resolveAgentConfig(cfg, agentId)?.skills);
}
export function resolveAgentModelPrimary(cfg: OpenClawConfig, agentId: string): string | undefined {
const raw = resolveAgentConfig(cfg, agentId)?.model;
if (!raw) {
function resolveModelPrimary(raw: unknown): string | undefined {
if (typeof raw === "string") {
const trimmed = raw.trim();
return trimmed || undefined;
}
if (!raw || typeof raw !== "object") {
return undefined;
}
if (typeof raw === "string") {
return raw.trim() || undefined;
const primary = (raw as { primary?: unknown }).primary;
if (typeof primary !== "string") {
return undefined;
}
const primary = raw.primary?.trim();
return primary || undefined;
const trimmed = primary.trim();
return trimmed || undefined;
}
export function resolveAgentExplicitModelPrimary(
cfg: OpenClawConfig,
agentId: string,
): string | undefined {
const raw = resolveAgentConfig(cfg, agentId)?.model;
return resolveModelPrimary(raw);
}
export function resolveAgentEffectiveModelPrimary(
cfg: OpenClawConfig,
agentId: string,
): string | undefined {
return (
resolveAgentExplicitModelPrimary(cfg, agentId) ??
resolveModelPrimary(cfg.agents?.defaults?.model)
);
}
// Backward-compatible alias. Prefer explicit/effective helpers at new call sites.
export function resolveAgentModelPrimary(cfg: OpenClawConfig, agentId: string): string | undefined {
return resolveAgentExplicitModelPrimary(cfg, agentId);
}
export function resolveAgentModelFallbacksOverride(
@@ -178,10 +206,7 @@ export function resolveEffectiveModelFallbacks(params: {
if (!params.hasSessionModelOverride) {
return agentFallbacksOverride;
}
const defaultFallbacks =
typeof params.cfg.agents?.defaults?.model === "object"
? (params.cfg.agents.defaults.model.fallbacks ?? [])
: [];
const defaultFallbacks = resolveAgentModelFallbackValues(params.cfg.agents?.defaults?.model);
return agentFallbacksOverride ?? defaultFallbacks;
}

View File

@@ -1,4 +1,8 @@
import type { OpenClawConfig } from "../config/config.js";
import {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,
} from "../config/model-input.js";
import {
ensureAuthProfileStore,
getSoonestCooldownExpiry,
@@ -151,26 +155,13 @@ function resolveImageFallbackCandidates(params: {
if (params.modelOverride?.trim()) {
addRaw(params.modelOverride, false);
} else {
const imageModel = params.cfg?.agents?.defaults?.imageModel as
| { primary?: string }
| string
| undefined;
const primary = typeof imageModel === "string" ? imageModel.trim() : imageModel?.primary;
const primary = resolveAgentModelPrimaryValue(params.cfg?.agents?.defaults?.imageModel);
if (primary?.trim()) {
addRaw(primary, false);
}
}
const imageFallbacks = (() => {
const imageModel = params.cfg?.agents?.defaults?.imageModel as
| { fallbacks?: string[] }
| string
| undefined;
if (imageModel && typeof imageModel === "object") {
return imageModel.fallbacks ?? [];
}
return [];
})();
const imageFallbacks = resolveAgentModelFallbackValues(params.cfg?.agents?.defaults?.imageModel);
for (const raw of imageFallbacks) {
addRaw(raw, true);
@@ -220,14 +211,7 @@ function resolveFallbackCandidates(params: {
if (!sameModelCandidate(normalizedPrimary, configuredPrimary)) {
return []; // Override model failed → go straight to configured default
}
const model = params.cfg?.agents?.defaults?.model as
| { fallbacks?: string[] }
| string
| undefined;
if (model && typeof model === "object") {
return model.fallbacks ?? [];
}
return [];
return resolveAgentModelFallbackValues(params.cfg?.agents?.defaults?.model);
})();
for (const raw of modelFallbacks) {

View File

@@ -1,6 +1,7 @@
import type { OpenClawConfig } from "../config/config.js";
import { resolveAgentModelPrimaryValue, toAgentModelListLike } from "../config/model-input.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { resolveAgentConfig, resolveAgentModelPrimary } from "./agent-scope.js";
import { resolveAgentConfig, resolveAgentEffectiveModelPrimary } from "./agent-scope.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
import type { ModelCatalogEntry } from "./model-catalog.js";
import { normalizeGoogleModelId } from "./models-config.providers.js";
@@ -259,13 +260,7 @@ export function resolveConfiguredModelRef(params: {
defaultProvider: string;
defaultModel: string;
}): ModelRef {
const rawModel = (() => {
const raw = params.cfg.agents?.defaults?.model as { primary?: string } | string | undefined;
if (typeof raw === "string") {
return raw.trim();
}
return raw?.primary?.trim() ?? "";
})();
const rawModel = resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model) ?? "";
if (rawModel) {
const trimmed = rawModel.trim();
const aliasIndex = buildModelAliasIndex({
@@ -303,7 +298,7 @@ export function resolveDefaultModelForAgent(params: {
agentId?: string;
}): ModelRef {
const agentModelOverride = params.agentId
? resolveAgentModelPrimary(params.cfg, params.agentId)
? resolveAgentEffectiveModelPrimary(params.cfg, params.agentId)
: undefined;
const cfg =
agentModelOverride && agentModelOverride.length > 0
@@ -314,9 +309,7 @@ export function resolveDefaultModelForAgent(params: {
defaults: {
...params.cfg.agents?.defaults,
model: {
...(typeof params.cfg.agents?.defaults?.model === "object"
? params.cfg.agents.defaults.model
: undefined),
...toAgentModelListLike(params.cfg.agents?.defaults?.model),
primary: agentModelOverride,
},
},
@@ -357,7 +350,7 @@ export function resolveSubagentSpawnModelSelection(params: {
cfg: params.cfg,
agentId: params.agentId,
}) ??
normalizeModelSelection(params.cfg.agents?.defaults?.model?.primary) ??
normalizeModelSelection(resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model)) ??
`${runtimeDefault.provider}/${runtimeDefault.model}`
);
}

View File

@@ -1,6 +1,7 @@
import { randomBytes } from "node:crypto";
import fs from "node:fs/promises";
import type { ThinkLevel } from "../../auto-reply/thinking.js";
import { resolveAgentModelFallbackValues } from "../../config/model-input.js";
import { generateSecureToken } from "../../infra/secure-random.js";
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import type { PluginHookBeforeAgentStartResult } from "../../plugins/types.js";
@@ -231,7 +232,7 @@ export async function runEmbeddedPiAgent(
let modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
const agentDir = params.agentDir ?? resolveOpenClawAgentDir();
const fallbackConfigured =
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
resolveAgentModelFallbackValues(params.config?.agents?.defaults?.model).length > 0;
await ensureOpenClawModelsJson(params.config, agentDir);
// Run before_model_resolve hooks early so plugins can override the

View File

@@ -1,5 +1,9 @@
import type { AssistantMessage } from "@mariozechner/pi-ai";
import type { OpenClawConfig } from "../../config/config.js";
import {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,
} from "../../config/model-input.js";
import { extractAssistantText } from "../pi-embedded-utils.js";
export type ImageModelConfig = { primary?: string; fallbacks?: string[] };
@@ -51,12 +55,8 @@ export function coerceImageAssistantText(params: {
}
export function coerceImageModelConfig(cfg?: OpenClawConfig): ImageModelConfig {
const imageModel = cfg?.agents?.defaults?.imageModel as
| { primary?: string; fallbacks?: string[] }
| string
| undefined;
const primary = typeof imageModel === "string" ? imageModel.trim() : imageModel?.primary;
const fallbacks = typeof imageModel === "object" ? (imageModel?.fallbacks ?? []) : [];
const primary = resolveAgentModelPrimaryValue(cfg?.agents?.defaults?.imageModel);
const fallbacks = resolveAgentModelFallbackValues(cfg?.agents?.defaults?.imageModel);
return {
...(primary?.trim() ? { primary: primary.trim() } : {}),
...(fallbacks.length > 0 ? { fallbacks } : {}),